=== ./gui-client/src/main/kotlin/guiclient/network/ClientProvider.kt ===
package guiclient.network

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import models.HumanBeing
import network.NetworkManager as ClientNetworkManager
import network.AuthRequest
import network.AuthResponse
import network.AuthType
import network.Request
import network.Response
import java.security.MessageDigest
import java.util.UUID

class ClientProvider(
    host: String,
    port: Int,
) {
    private val net = ClientNetworkManager(host, port)

    suspend fun auth(login: String, password: String, type: AuthType): AuthResponse? =
        withContext(Dispatchers.IO) {
            net.sendRequest(AuthRequest(login, md5Hash(password), type))
        }

    suspend fun execute(commandName: String, args: List<String> = emptyList(), humanBeing: HumanBeing? = null): Response? =
        withContext(Dispatchers.IO) {
            val request = Request(
                commandName = commandName,
                args = args,
                humanBeing = humanBeing,
                jwt = net.getToken()
            )
            net.sendRequest(request)
        }

    fun getToken(): String = net.getToken()

    fun setToken(token: String) = net.setToken(token)

    fun clearToken() = net.clearToken()

    fun getUUID(): UUID = net.getUUID()

    fun setUUID(id: String) = net.setUUID(id)

    fun close() = net.close()

    private fun md5Hash(input: String): String {
        val md = MessageDigest.getInstance("MD5")
        val hashBytes = md.digest(input.toByteArray())
        return hashBytes.joinToString("") { String.format("%02x", it) }
    }
}
=== ./gui-client/src/main/kotlin/util/StreamUtils.kt ===
package util

import javafx.collections.FXCollections
import javafx.collections.ObservableList
import model.HumanBeingProperty
import java.util.Comparator
import java.util.stream.Collectors
import java.util.stream.Stream

object StreamUtils {

    fun filter(
        items: List<HumanBeingProperty>,
        propertyExtractor: (HumanBeingProperty) -> String,
        query: String,
    ): Stream<HumanBeingProperty> {
        if (query.isBlank()) return items.stream()
        val lower = query.trim().lowercase()
        return items.stream()
            .filter { propertyExtractor(it).lowercase().contains(lower) }
    }

    fun sort(
        stream: Stream<HumanBeingProperty>,
        comparator: Comparator<HumanBeingProperty>?,
    ): Stream<HumanBeingProperty> {
        if (comparator == null) return stream
        return stream.sorted(comparator)
    }

    fun applyFiltersAndSort(
        items: List<HumanBeingProperty>,
        filters: Map<String, String>,
        comparators: List<Comparator<HumanBeingProperty>>,
    ): ObservableList<HumanBeingProperty> {
        var stream = items.stream()

        for ((key, query) in filters) {
            if (query.isNotBlank()) {
                val lower = query.trim().lowercase()
                stream = stream.filter { prop ->
                    val fieldValue = when (key) {
                        "name" -> prop.name.value
                        "coordinates.x" -> prop.coordinateX.value.toString()
                        "coordinates.y" -> prop.coordinateY.value.toString()
                        "creationDate" -> prop.creationDate.value.toString()
                        "realHero" -> prop.realHero.value.toString()
                        "hasToothpick" -> prop.hasToothpick.value.toString()
                        "impactSpeed" -> prop.impactSpeed.value.toString()
                        "soundtrackName" -> prop.soundtrackName.value
                        "minutesOfWaiting" -> prop.minutesOfWaiting.value.toString()
                        "weaponType" -> prop.weaponType.value?.name ?: ""
                        "car.cool" -> prop.carCool.value.toString()
                        else -> ""
                    }
                    fieldValue.lowercase().contains(lower)
                }
            }
        }

        if (comparators.isNotEmpty()) {
            val combined = comparators.reduce { a, b -> a.thenComparing(b) }
            stream = stream.sorted(combined)
        }

        return stream.collect(Collectors.collectingAndThen(
            Collectors.toList(),
            { FXCollections.observableArrayList(it) }
        ))
    }
}
=== ./gui-client/src/main/kotlin/Main.kt ===
import javafx.application.Application
import javafx.stage.Stage
import model.Session
import view.AuthView
import view.MainView

class App : Application() {

    override fun start(stage: Stage) {
        AuthView(stage) {
            val mainStage = Stage()
            MainView(mainStage).show()
        }.show()
    }

    override fun stop() {
        Session.client?.close()
    }
}

fun main() {
    Application.launch(App::class.java)
}
=== ./gui-client/src/main/kotlin/controller/AuthController.kt ===
package controller

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import localization.AppLocale
import model.Session
import network.AuthType
import guiclient.network.ClientProvider
import javafx.application.Platform

class AuthController {

    private val scope = CoroutineScope(Dispatchers.Default)

    fun authenticate(
        host: String,
        port: Int,
        username: String,
        password: String,
        isLogin: Boolean,
        onResult: (Boolean, String) -> Unit,
    ) {
        scope.launch {
            Session.client?.close()

            val provider = ClientProvider(host, port)

            val type = if (isLogin) AuthType.LOGIN else AuthType.REGISTER
            val response = provider.auth(username, password, type)

            if (response == null) {
                Platform.runLater { onResult(false, AppLocale.get("auth.error.server")) }
                return@launch
            }

            if (response.success) {
                provider.setToken(response.accessToken ?: "")
                provider.setUUID(response.id ?: "")
                Session.client = provider
                Session.username = username
                Session.userId = response.id?.let { kotlin.runCatching { java.util.UUID.fromString(it) }.getOrNull() }
                Platform.runLater { onResult(true, "") }
            } else {
                val errorKey = if (isLogin) "auth.error.failed" else "auth.error.register.failed"
                Platform.runLater { onResult(false, response.message.ifEmpty { AppLocale.get(errorKey) }) }
            }
        }
    }
}
=== ./gui-client/src/main/kotlin/controller/MainController.kt ===
package controller

import javafx.application.Platform
import javafx.collections.FXCollections
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import kotlinx.coroutines.*
import localization.AppLocale
import model.HumanBeingProperty
import model.Session
import util.StreamUtils
import java.util.Comparator
import java.util.UUID

class MainController {

    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
    private var pollingJob: Job? = null

    val filteredList: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()

    private var currentFilters: Map<String, String> = emptyMap()
    private var currentComparators: List<Comparator<HumanBeingProperty>> = emptyList()

    private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
        reapplyFiltersAndSort()
    }

    fun start() {
        Session.collection.addListener(collectionListener)
        startPolling()
    }

    fun stop() {
        pollingJob?.cancel()
        scope.cancel()
        Session.collection.removeListener(collectionListener)
    }

    private fun startPolling() {
        pollingJob = scope.launch {
            while (isActive) {
                delay(3000)
                refreshCollection()
            }
        }
    }

    fun requestRefresh() {
        scope.launch {
            refreshCollection()
        }
    }

    private suspend fun refreshCollection() {
        val response = Session.client?.execute("show") ?: return

        val currentIds = Session.collection.map { it.id.value }.toSet()

        val serverItems = parseShowResponse(response.message) ?: return
        val serverIds = serverItems.map { it.id.value }.toSet()

        val toRemove = Session.collection.filter { it.id.value !in serverIds }
        val toAdd = serverItems.filter { it.id.value !in currentIds }
        val toUpdate = serverItems.filter { item ->
            val existing = Session.collection.find { it.id.value == item.id.value }
            existing != null && existing.name.value != item.name.value
        }

        Platform.runLater {
            Session.collection.removeAll(toRemove)
            Session.collection.addAll(toAdd)
            for (updated in toUpdate) {
                val idx = Session.collection.indexOfFirst { it.id.value == updated.id.value }
                if (idx >= 0) {
                    Session.collection[idx] = updated
                }
            }
        }
    }

    private fun parseShowResponse(message: String): List<HumanBeingProperty>? {
        if (message.isBlank()) return emptyList()
        val lines = message.lines().filter { it.isNotBlank() }

        val items = mutableListOf<HumanBeingProperty>()
        var currentLines = mutableListOf<String>()
        var braceCount = 0

        for (line in lines) {
            currentLines.add(line)
            braceCount += line.count { it == '{' }
            braceCount -= line.count { it == '}' }
            if (braceCount == 0 && currentLines.isNotEmpty()) {
                val parsed = parseHumanBeingBlock(currentLines)
                if (parsed != null) items.add(parsed)
                currentLines.clear()
            }
        }

        return items
    }

    private fun parseHumanBeingBlock(lines: List<String>): HumanBeingProperty? {
        val text = lines.joinToString("\n")

        val id = extractValue(text, "id\\s*=\\s*([\\w-]+)")?.let { safeUUID(it) }
        val ownerId = extractValue(text, "owner\\s*=\\s*([\\w-]+)")?.let { safeUUID(it) } ?: return null
        val name = extractValue(text, "name\\s*=\\s*(.+?)\\s*\\(a\\.k\\.a\\. Ryan Gosling\\)") ?: return null
        val coordX = extractValue(text, "x=(-?\\d+)")?.toLongOrNull() ?: 0L
        val coordY = extractValue(text, "y=(-?\\d+)")?.toIntOrNull() ?: 0
        val creationDate = extractValue(text, "creationDate\\s*=\\s*(\\S+)")?.let { safeLocalDate(it) }
        val realHero = extractValue(text, "realHero\\s*=\\s*(\\w+)")?.toBooleanStrictOrNull() ?: false
        val hasToothpick = extractValue(text, "hasToothpick\\s*=\\s*(\\w+)")?.toBooleanStrictOrNull() ?: false
        val impactSpeed = extractValue(text, "impactSpeed\\s*=\\s*(-?\\d+\\.?\\d*)")?.toDoubleOrNull() ?: 0.0
        val soundtrackName = extractValue(text, "soundtrackName\\s*=\\s*(.+)") ?: ""
        val minutesOfWaiting = extractValue(text, "minutesOfWait\\s*=\\s*(-?\\d+\\.?\\d*)")?.toFloatOrNull() ?: 0f
        val weaponType = extractValue(text, "weaponType\\s*=\\s*(\\w+)")?.let { safeWeaponType(it) }
        val carCool = extractValue(text, "car\\.cool\\s*=\\s*(\\w+)")?.toBooleanStrictOrNull() ?: false

        val date = creationDate ?: java.time.LocalDate.now()

        val human = models.HumanBeing(
            id = id,
            ownerId = ownerId,
            name = name,
            coordinates = models.Coordinates(coordX, coordY),
            creationDate = date,
            realHero = realHero,
            hasToothpick = hasToothpick,
            impactSpeed = impactSpeed,
            soundtrackName = soundtrackName,
            minutesOfWaiting = minutesOfWaiting,
            weaponType = weaponType,
            car = models.Car(carCool),
        )
        return HumanBeingProperty(human)
    }

    private fun extractValue(text: String, pattern: String): String? {
        val regex = Regex(pattern)
        return regex.find(text)?.groupValues?.getOrNull(1)
    }

    private fun safeUUID(value: String): UUID? {
        return try { UUID.fromString(value) } catch (_: Exception) { null }
    }

    private fun safeLocalDate(value: String): java.time.LocalDate? {
        return try { java.time.LocalDate.parse(value) } catch (_: Exception) { null }
    }

    private fun safeWeaponType(value: String): models.WeaponType? {
        return try { models.WeaponType.valueOf(value.uppercase()) } catch (_: Exception) { null }
    }

    fun updateFilters(filters: Map<String, String>) {
        currentFilters = filters
        reapplyFiltersAndSort()
    }

    fun updateComparators(comparators: List<Comparator<HumanBeingProperty>>) {
        currentComparators = comparators
        reapplyFiltersAndSort()
    }

    private fun reapplyFiltersAndSort() {
        val result = StreamUtils.applyFiltersAndSort(
            Session.collection,
            currentFilters,
            currentComparators,
        )
        Platform.runLater {
            filteredList.setAll(result)
        }
    }

    fun executeCommand(
        name: String,
        args: List<String> = emptyList(),
        humanBeing: models.HumanBeing? = null,
        callback: (Boolean, String) -> Unit = { _, _ -> },
    ) {
        scope.launch {
            val response = Session.client?.execute(name, args, humanBeing)
            val success = response?.success ?: false
            val message = response?.message ?: AppLocale.get("auth.error.server")
            Platform.runLater { callback(success, message) }
        }
    }

    fun deleteObject(humanBeingProperty: HumanBeingProperty, callback: (Boolean, String) -> Unit) {
        val id = humanBeingProperty.id.value ?: return
        executeCommand(name = "remove_by_id", args = listOf(id.toString()), callback = callback)
    }
}
=== ./gui-client/src/main/kotlin/model/HumanBeingProperty.kt ===
package model

import javafx.beans.property.*
import models.Car
import models.Coordinates
import models.HumanBeing
import models.WeaponType
import java.time.LocalDate
import java.util.UUID

class HumanBeingProperty(humanBeing: HumanBeing) {
    val id = SimpleObjectProperty<UUID?>(humanBeing.id)
    val ownerId = SimpleObjectProperty<UUID>(humanBeing.ownerId)
    val name = SimpleStringProperty(humanBeing.name)
    val coordinateX = SimpleObjectProperty<Long>(humanBeing.coordinates.x)
    val coordinateY = SimpleObjectProperty<Int>(humanBeing.coordinates.y)
    val creationDate = SimpleObjectProperty<LocalDate>(humanBeing.creationDate)
    val realHero = SimpleBooleanProperty(humanBeing.realHero)
    val hasToothpick = SimpleBooleanProperty(humanBeing.hasToothpick)
    val impactSpeed = SimpleObjectProperty<Double>(humanBeing.impactSpeed)
    val soundtrackName = SimpleStringProperty(humanBeing.soundtrackName)
    val minutesOfWaiting = SimpleObjectProperty<Float>(humanBeing.minutesOfWaiting)
    val weaponType = SimpleObjectProperty<WeaponType?>(humanBeing.weaponType)
    val carCool = SimpleBooleanProperty(humanBeing.car.cool)

    fun toHumanBeing(): HumanBeing = HumanBeing(
        id = id.value,
        ownerId = ownerId.value,
        name = name.value,
        coordinates = Coordinates(coordinateX.value, coordinateY.value),
        creationDate = creationDate.value,
        realHero = realHero.value,
        hasToothpick = hasToothpick.value,
        impactSpeed = impactSpeed.value,
        soundtrackName = soundtrackName.value,
        minutesOfWaiting = minutesOfWaiting.value,
        weaponType = weaponType.value,
        car = Car(carCool.value),
    )

    companion object {
        fun fromHumanBeing(h: HumanBeing): HumanBeingProperty = HumanBeingProperty(h)
    }
}
=== ./gui-client/src/main/kotlin/model/Session.kt ===
package model

import javafx.collections.FXCollections
import javafx.collections.ObservableList
import guiclient.network.ClientProvider
import java.util.UUID

object Session {
    var client: ClientProvider? = null

    var token: String = ""
    var userId: UUID? = null
    var username: String = ""

    val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()

    val userIdFilter: UUID?
        get() = userId

    fun isOwnObject(ownerId: UUID): Boolean = userId == ownerId

    fun clear() {
        token = ""
        userId = null
        username = ""
        collection.clear()
    }
}
=== ./gui-client/src/main/kotlin/view/CollectionCanvas.kt ===
package view

import javafx.animation.AnimationTimer
import javafx.scene.canvas.Canvas
import javafx.scene.canvas.GraphicsContext
import javafx.scene.control.Alert
import javafx.scene.control.ButtonType
import javafx.scene.input.MouseEvent
import javafx.scene.paint.Color
import javafx.scene.text.Font
import localization.AppLocale
import model.HumanBeingProperty
import model.Session

class CollectionCanvas : Canvas() {

    private val gc: GraphicsContext = graphicsContext2D
    private var animationTime = 0.0

    private var canvasWidth: Double = 0.0
    private var canvasHeight: Double = 0.0

    private val padding = 40.0
    private val defaultRange = 200.0

    private var scaleX: Double = 1.0
    private var scaleY: Double = 1.0
    private var offsetX: Double = 0.0
    private var offsetY: Double = 0.0
    private var minRadius = 8.0
    private var maxRadius = 35.0

    private val renderTimer: AnimationTimer = object : AnimationTimer() {
        override fun handle(now: Long) {
            animationTime = now / 1_000_000_000.0
            render()
        }
    }

    init {
        widthProperty().addListener { _, _, _ -> scheduleRender() }
        heightProperty().addListener { _, _, _ -> scheduleRender() }
        addEventHandler(MouseEvent.MOUSE_CLICKED, this::onClick)
        renderTimer.start()
        Session.collection.addListener { _: javafx.collections.ListChangeListener.Change<out HumanBeingProperty>? ->
            scheduleRender()
        }
    }

    private fun scheduleRender() {
        render()
    }

    private fun render() {
        canvasWidth = width
        canvasHeight = height
        if (canvasWidth <= 0.0 || canvasHeight <= 0.0) return

        gc.clearRect(0.0, 0.0, canvasWidth, canvasHeight)

        drawBackground()

        val items = Session.collection.toList()
        if (items.isEmpty()) {
            drawEmptyMessage()
            return
        }

        computeScale(items)
        val glowIntensity = computeGlowIntensity()

        for (item in items) {
            drawItem(item, glowIntensity)
        }
    }

    private fun drawBackground() {
        gc.fill = Color.web("#1a1a2e")
        gc.fillRect(0.0, 0.0, canvasWidth, canvasHeight)

        gc.stroke = Color.web("#16213e")
        gc.lineWidth = 0.5
        val gridSize = 40.0
        var x = 0.0
        while (x <= canvasWidth) {
            gc.strokeLine(x, 0.0, x, canvasHeight)
            x += gridSize
        }
        var y = 0.0
        while (y <= canvasHeight) {
            gc.strokeLine(0.0, y, canvasWidth, y)
            y += gridSize
        }
    }

    private fun drawEmptyMessage() {
        gc.fill = Color.GRAY
        gc.font = Font(18.0)
        gc.fillText(AppLocale.get("visualization.title"), canvasWidth / 2 - 60, canvasHeight / 2)
    }

    private fun computeScale(items: List<HumanBeingProperty>) {
        val minX = items.minOf { it.coordinateX.value }
        val maxX = items.maxOf { it.coordinateX.value }
        val minY = items.minOf { it.coordinateY.value }
        val maxY = items.maxOf { it.coordinateY.value }

        val rangeX = if (maxX == minX) defaultRange else (maxX - minX).toDouble()
        val rangeY = if (maxY == minY) defaultRange else (maxY - minY).toDouble()

        val availWidth = canvasWidth - 2 * padding
        val availHeight = canvasHeight - 2 * padding

        scaleX = availWidth / rangeX
        scaleY = availHeight / rangeY

        offsetX = padding - minX * scaleX
        offsetY = padding - minY * scaleY
    }

    private fun computeGlowIntensity(): Double {
        return 0.4 + 0.4 * (1.0 + kotlin.math.sin(animationTime * 3.0)) / 2.0
    }

    private fun drawItem(item: HumanBeingProperty, glowIntensity: Double) {
        val px = worldToScreenX(item.coordinateX.value.toDouble())
        val py = worldToScreenY(item.coordinateY.value.toDouble())
        val radius = mapImpactSpeed(item.impactSpeed.value)

        val color = ownerColor(item.ownerId.value)
        val glowColor = Color(color.red, color.green, color.blue, glowIntensity * 0.5)

        gc.save()

        gc.setFill(glowColor)
        gc.fillOval(px - radius * 1.8, py - radius * 1.8, radius * 3.6, radius * 3.6)

        gc.setFill(color)
        gc.fillOval(px - radius, py - radius, radius * 2, radius * 2)

        gc.setStroke(Color.WHITE)
        gc.setLineWidth(1.5)
        gc.strokeOval(px - radius, py - radius, radius * 2, radius * 2)

        gc.setFill(Color.WHITE)
        gc.setFont(Font(10.0))
        val label = item.name.value.take(10)
        gc.fillText(label, px - radius, py - radius - 4)

        gc.restore()
    }

    private fun worldToScreenX(worldX: Double): Double = worldX * scaleX + offsetX

    private fun worldToScreenY(worldY: Double): Double = worldY * scaleY + offsetY

    private fun screenToWorldX(screenX: Double): Double = (screenX - offsetX) / scaleX

    private fun screenToWorldY(screenY: Double): Double = (screenY - offsetY) / scaleY

    private fun mapImpactSpeed(speed: Double): Double {
        val items = Session.collection
        if (items.isEmpty()) return minRadius

        val minSpeed = items.minOf { it.impactSpeed.value }
        val maxSpeed = items.maxOf { it.impactSpeed.value }

        if (maxSpeed == minSpeed) return (minRadius + maxRadius) / 2

        return minRadius + (speed - minSpeed) / (maxSpeed - minSpeed) * (maxRadius - minRadius)
    }

    private fun ownerColor(ownerId: java.util.UUID): Color {
        val hue = (ownerId.hashCode() and 0x7FFFFFFF) % 360
        return Color.hsb(hue.toDouble(), 0.7, 0.9)
    }

    private fun onClick(event: MouseEvent) {
        val mx = event.x
        val my = event.y

        val items = Session.collection.toList()
        val hitRadius = 15.0

        for (item in items) {
            val px = worldToScreenX(item.coordinateX.value.toDouble())
            val py = worldToScreenY(item.coordinateY.value.toDouble())
            val r = mapImpactSpeed(item.impactSpeed.value)
            val dx = mx - px
            val dy = my - py
            if (dx * dx + dy * dy <= (r + hitRadius) * (r + hitRadius)) {
                showItemInfo(item)
                return
            }
        }
    }

    private fun showItemInfo(item: HumanBeingProperty) {
        val info = buildString {
            appendLine("${AppLocale.get("info.id")}: ${item.id.value}")
            appendLine("${AppLocale.get("table.name")}: ${item.name.value}")
            appendLine("X: ${item.coordinateX.value}, Y: ${item.coordinateY.value}")
            appendLine("${AppLocale.get("table.creationDate")}: ${item.creationDate.value}")
            appendLine("${AppLocale.get("table.realHero")}: ${item.realHero.value}")
            appendLine("${AppLocale.get("table.hasToothpick")}: ${item.hasToothpick.value}")
            appendLine("${AppLocale.get("table.impactSpeed")}: ${item.impactSpeed.value}")
            appendLine("${AppLocale.get("table.soundtrackName")}: ${item.soundtrackName.value}")
            appendLine("${AppLocale.get("table.minutesOfWaiting")}: ${item.minutesOfWaiting.value}")
            appendLine("${AppLocale.get("table.weaponType")}: ${item.weaponType.value?.name ?: "—"}")
            appendLine("${AppLocale.get("table.car.cool")}: ${item.carCool.value}")
            appendLine("${AppLocale.get("info.owner")}: ${item.ownerId.value}")
        }
        val alert = Alert(Alert.AlertType.INFORMATION, info.trimEnd(), ButtonType.OK)
        alert.title = AppLocale.get("info.title")
        alert.headerText = item.name.value
        alert.show()
    }

    override fun isResizable(): Boolean = true

    override fun prefWidth(height: Double): Double = 400.0

    override fun prefHeight(width: Double): Double = 400.0
}
=== ./gui-client/src/main/kotlin/view/EditDialog.kt ===
package view

import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.control.*
import javafx.scene.layout.GridPane
import javafx.scene.layout.VBox
import javafx.stage.Modality
import javafx.stage.Stage
import javafx.stage.Window
import localization.AppLocale
import models.Car
import models.Coordinates
import models.HumanBeing
import models.WeaponType
import model.Session
import java.time.LocalDate

class EditDialog(private val owner: Window, private val existing: HumanBeing? = null) {

    private val dialog = Stage()

    private val nameField = TextField()
    private val coordXField = TextField()
    private val coordYField = TextField()
    private val realHeroCheck = CheckBox()
    private val hasToothpickCheck = CheckBox()
    private val impactSpeedField = TextField()
    private val soundtrackField = TextField()
    private val minutesField = TextField()
    private val weaponTypeCombo = ComboBox<WeaponTypeOption>()
    private val carCoolCheck = CheckBox()

    private var result: HumanBeing? = null

    fun showAndWait(): HumanBeing? {
        dialog.initModality(Modality.WINDOW_MODAL)
        dialog.initOwner(owner)
        dialog.scene = javafx.scene.Scene(buildUI(), 420.0, 520.0)
        dialog.isResizable = false

        if (existing != null) populateFields(existing)
        updateTitle()
        dialog.showAndWait()
        return result
    }

    private fun buildUI(): VBox {
        val form = GridPane()
        form.hgap = 8.0
        form.vgap = 8.0
        form.padding = Insets(16.0)
        form.alignment = Pos.CENTER_LEFT

        var row = 0

        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.name") }, nameField)
        row++

        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.x") }, coordXField)
        row++
        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.y") }, coordYField)
        row++

        realHeroCheck.isSelected = true
        form.addRow(row, realHeroCheck.also { AppLocale.bindLabel(it, "table.realHero") })
        row++

        hasToothpickCheck.isSelected = true
        form.addRow(row, hasToothpickCheck.also { AppLocale.bindLabel(it, "table.hasToothpick") })
        row++

        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.impactSpeed") }, impactSpeedField)
        row++
        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.soundtrackName") }, soundtrackField)
        row++
        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.minutesOfWaiting") }, minutesField)
        row++

        weaponTypeCombo.items.add(WeaponTypeOption("—", null))
        WeaponType.entries.forEach { wt ->
            weaponTypeCombo.items.add(WeaponTypeOption(wt.name, wt))
        }
        weaponTypeCombo.selectionModel.selectFirst()
        form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.weaponType") }, weaponTypeCombo)
        row++

        carCoolCheck.isSelected = true
        form.addRow(row, carCoolCheck.also { AppLocale.bindLabel(it, "table.car.cool") })

        val buttonBar = javafx.scene.layout.HBox(8.0).apply {
            alignment = Pos.CENTER_RIGHT
            padding = Insets(0.0, 16.0, 16.0, 16.0)
            children.addAll(
                Button().also {
                    AppLocale.bindLabel(it, "dialog.ok")
                    it.setOnAction { onOk() }
                },
                Button().also {
                    AppLocale.bindLabel(it, "dialog.cancel")
                    it.setOnAction { dialog.close() }
                },
            )
        }

        return VBox(8.0, form, buttonBar)
    }

    private fun updateTitle() {
        val key = if (existing != null) "dialog.title.edit" else "dialog.title.add"
        dialog.title = AppLocale.get(key)
    }

    private fun populateFields(h: HumanBeing) {
        nameField.text = h.name
        coordXField.text = h.coordinates.x.toString()
        coordYField.text = h.coordinates.y.toString()
        realHeroCheck.isSelected = h.realHero
        hasToothpickCheck.isSelected = h.hasToothpick
        impactSpeedField.text = h.impactSpeed.toString()
        soundtrackField.text = h.soundtrackName
        minutesField.text = h.minutesOfWaiting.toString()
        if (h.weaponType != null) {
            val selected = weaponTypeCombo.items.find { it.type == h.weaponType }
            if (selected != null) weaponTypeCombo.selectionModel.select(selected)
        }
        carCoolCheck.isSelected = h.car.cool
    }

    private fun onOk() {
        val name = nameField.text.trim()
        if (name.isEmpty()) {
            showError(AppLocale.get("dialog.error.name"))
            return
        }

        val coordX = coordXField.text.trim().toLongOrNull()
        if (coordX == null) { showError("X: " + AppLocale.get("dialog.error.number")); return }
        val coordY = coordYField.text.trim().toIntOrNull()
        if (coordY == null) { showError("Y: " + AppLocale.get("dialog.error.number")); return }

        val impactSpeed = impactSpeedField.text.trim().toDoubleOrNull()
        if (impactSpeed == null) { showError("impactSpeed: " + AppLocale.get("dialog.error.number")); return }

        val minutes = minutesField.text.trim().toFloatOrNull()
        if (minutes == null) { showError("minutesOfWaiting: " + AppLocale.get("dialog.error.number")); return }

        val soundtrack = soundtrackField.text.trim()

        result = HumanBeing(
            id = existing?.id,
            ownerId = existing?.ownerId ?: Session.userId ?: run { showError(AppLocale.get("dialog.error.no.user")); return },
            name = name,
            coordinates = Coordinates(coordX, coordY),
            creationDate = existing?.creationDate ?: LocalDate.now(),
            realHero = realHeroCheck.isSelected,
            hasToothpick = hasToothpickCheck.isSelected,
            impactSpeed = impactSpeed,
            soundtrackName = soundtrack,
            minutesOfWaiting = minutes,
            weaponType = weaponTypeCombo.selectionModel.selectedItem?.type,
            car = Car(carCoolCheck.isSelected),
        )
        dialog.close()
    }

    private fun showError(message: String) {
        val alert = Alert(Alert.AlertType.ERROR, message, ButtonType.OK)
        alert.initOwner(dialog)
        alert.show()
    }

    private data class WeaponTypeOption(val label: String, val type: WeaponType?) {
        override fun toString() = label
    }
}
=== ./gui-client/src/main/kotlin/view/MainView.kt ===
package view

import controller.MainController
import javafx.beans.property.SimpleObjectProperty

import javafx.geometry.Insets
import javafx.geometry.Orientation
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.Scene
import view.CollectionCanvas
import javafx.scene.control.*
import javafx.scene.layout.*
import javafx.stage.Stage
import models.HumanBeing
import localization.AppLocale
import model.HumanBeingProperty
import model.Session
import java.util.Comparator

class MainView(private val stage: Stage) {

    private val controller = MainController()

    private val tableView = TableView<HumanBeingProperty>()
    private val canvas = CollectionCanvas()
    private val canvasPlaceholder = Label()
    private val usernameLabel = Label()
    private val localeCombo = ComboBox<LocaleDisplay>()
    private val addButton = Button()
    private val editButton = Button()
    private val deleteButton = Button()
    private val refreshButton = Button()

    private val filterFields = mutableMapOf<String, TextField>()

    private var sortColumn: String? = null
    private var sortAscending = true

    fun show() {
        updateTitle()
        val scene = Scene(buildUI(), 1100.0, 700.0)
        stage.scene = scene
        stage.minWidth = 800.0
        stage.minHeight = 500.0
        stage.show()

        controller.start()
    }

    private fun buildUI(): BorderPane {
        val root = BorderPane()

        root.top = buildToolbar()
        root.center = buildContent()
        root.bottom = buildCommandBar()

        return root
    }

    private fun buildToolbar(): ToolBar {
        AppLocale.bindLabel(usernameLabel, "main.user")
        updateUsernameText()

        val logoutItem = MenuItem()
        AppLocale.bind(logoutItem.textProperty(), "main.logout")
        logoutItem.setOnAction { onLogout() }

        val userMenu = MenuButton().also { menu ->
            AppLocale.bindLabel(menu, "main.user")
            menu.items.add(logoutItem)
        }

        AppLocale.availableLocales().forEach { loc ->
            localeCombo.items.add(LocaleDisplay(AppLocale.localeDisplayName(loc), loc))
        }
        val currentLocaleIndex = AppLocale.availableLocales().indexOf(AppLocale.currentLocale.value)
        if (currentLocaleIndex >= 0) localeCombo.selectionModel.select(currentLocaleIndex)
        localeCombo.setOnAction {
            val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
            AppLocale.currentLocale.set(selected.locale)
            updateLocale()
        }

        val spacer = Region()
        HBox.setHgrow(spacer, Priority.ALWAYS)

        val toolbar = ToolBar(
            Label().also { AppLocale.bindLabel(it, "main.user"); it.visibleProperty().bind(usernameLabel.visibleProperty()) },
            usernameLabel,
            Separator(Orientation.VERTICAL),
            Label().also { AppLocale.bindLabel(it, "main.locale") },
            localeCombo,
            spacer,
            userMenu,
        )
        return toolbar
    }

    private fun buildContent(): Node {
        val splitPane = SplitPane()
        splitPane.orientation = Orientation.HORIZONTAL

        splitPane.items.addAll(buildTablePane(), buildCanvasPane())

        splitPane.setDividerPositions(0.55)

        return splitPane
    }

    private fun buildTablePane(): Node {
        val filterBar = GridPane()
        filterBar.hgap = 4.0
        filterBar.vgap = 2.0
        filterBar.padding = Insets(4.0)

        val columns = listOf(
            "name" to { p: HumanBeingProperty -> p.name.value },
            "coordinates.x" to { p: HumanBeingProperty -> p.coordinateX.value.toString() },
            "coordinates.y" to { p: HumanBeingProperty -> p.coordinateY.value.toString() },
            "creationDate" to { p: HumanBeingProperty -> p.creationDate.value.toString() },
            "realHero" to { p: HumanBeingProperty -> p.realHero.value.toString() },
            "hasToothpick" to { p: HumanBeingProperty -> p.hasToothpick.value.toString() },
            "impactSpeed" to { p: HumanBeingProperty -> p.impactSpeed.value.toString() },
            "soundtrackName" to { p: HumanBeingProperty -> p.soundtrackName.value },
            "minutesOfWaiting" to { p: HumanBeingProperty -> p.minutesOfWaiting.value.toString() },
            "weaponType" to { p: HumanBeingProperty -> (p.weaponType.value?.name ?: "") },
            "car.cool" to { p: HumanBeingProperty -> p.carCool.value.toString() },
        )

        for ((i, col) in columns.withIndex()) {
            val (key, extractor) = col

            val headerLabel = Label()
            AppLocale.bindLabel(headerLabel, "table.$key")
            headerLabel.setOnMouseClicked {
                toggleSort(key)
            }
            headerLabel.style = "-fx-font-weight: bold; -fx-cursor: hand;"
            filterBar.add(headerLabel, i, 0)

            val filterField = TextField()
            filterField.promptText = AppLocale.get("main.filter")
            filterField.textProperty().addListener { _, _, _ ->
                applyFilters()
            }
            filterFields[key] = filterField
            filterBar.add(filterField, i, 1)

            val tableCol = TableColumn<HumanBeingProperty, String>()
            AppLocale.bind(tableCol.textProperty(), "table.$key")
            tableCol.cellValueFactory = javafx.util.Callback { features ->
                SimpleObjectProperty(extractor(features.value))
            }
            tableView.columns.add(tableCol)
        }

        val tableHolder = VBox(filterBar, tableView)
        VBox.setVgrow(tableView, Priority.ALWAYS)

        return tableHolder
    }

    private fun buildCanvasPane(): Node {
        AppLocale.bindLabel(canvasPlaceholder, "visualization.title")
        canvasPlaceholder.alignment = Pos.CENTER
        canvasPlaceholder.style = "-fx-font-size: 16; -fx-text-fill: gray;"

        val stackPane = StackPane(canvasPlaceholder, canvas)
        stackPane.prefWidth = 400.0

        canvas.widthProperty().bind(stackPane.widthProperty())
        canvas.heightProperty().bind(stackPane.heightProperty())

        return stackPane
    }

    private fun buildCommandBar(): HBox {
        AppLocale.bindLabel(addButton, "button.add")
        AppLocale.bindLabel(editButton, "button.edit")
        AppLocale.bindLabel(deleteButton, "button.delete")
        AppLocale.bindLabel(refreshButton, "button.refresh")

        addButton.setOnAction { onAdd() }
        editButton.setOnAction { onEdit() }
        deleteButton.setOnAction { onDelete() }
        refreshButton.setOnAction { controller.requestRefresh() }

        val bar = HBox(8.0, addButton, editButton, deleteButton, refreshButton)
        bar.padding = Insets(8.0)
        bar.alignment = Pos.CENTER_LEFT
        return bar
    }

    private fun toggleSort(key: String) {
        if (sortColumn == key) {
            sortAscending = !sortAscending
        } else {
            sortColumn = key
            sortAscending = true
        }

        val comparator: Comparator<HumanBeingProperty>? = sortColumn?.let { col ->
            val asc = sortAscending
            val comp = Comparator<HumanBeingProperty> { a, b ->
                val va = extractSortValue(a, col)
                val vb = extractSortValue(b, col)
                if (asc) va.compareTo(vb) else vb.compareTo(va)
            }
            comp
        }

        controller.updateComparators(if (comparator != null) listOf(comparator) else emptyList())
    }

    private fun extractSortValue(prop: HumanBeingProperty, key: String): String = when (key) {
        "name" -> prop.name.value
        "coordinates.x" -> prop.coordinateX.value.toString().padStart(20, '0')
        "coordinates.y" -> prop.coordinateY.value.toString().padStart(10, '0')
        "creationDate" -> prop.creationDate.value.toString()
        "realHero" -> if (prop.realHero.value) "1" else "0"
        "hasToothpick" -> if (prop.hasToothpick.value) "1" else "0"
        "impactSpeed" -> prop.impactSpeed.value.toString().padStart(20, '0')
        "soundtrackName" -> prop.soundtrackName.value
        "minutesOfWaiting" -> prop.minutesOfWaiting.value.toString().padStart(20, '0')
        "weaponType" -> prop.weaponType.value?.name ?: ""
        "car.cool" -> if (prop.carCool.value) "1" else "0"
        else -> ""
    }

    private fun applyFilters() {
        val filters = filterFields.mapValues { it.value.text }
        controller.updateFilters(filters)
    }

    init {
        tableView.items = controller.filteredList
    }

    private fun updateUsernameText() {
        usernameLabel.text = Session.username.ifEmpty { "—" }
    }

    private fun updateLocale() {
        updateTitle()
        updateUsernameText()
        filterFields.forEach { (_, field) ->
            field.promptText = AppLocale.get("main.filter")
        }
    }

    private fun updateTitle() {
        stage.title = AppLocale.get("main.title")
    }

    private fun onLogout() {
        controller.stop()
        Session.client?.clearToken()
        Session.clear()
        stage.close()
        AuthView(Stage()).show()
    }

    private fun onAdd() {
        val dialog = EditDialog(stage.scene.window)
        val humanBeing = dialog.showAndWait() ?: return
        controller.executeCommand(name = "add", humanBeing = humanBeing) { success, message ->
            if (success) controller.requestRefresh() else showError(message)
        }
    }

    private fun onEdit() {
        val selected = tableView.selectionModel.selectedItem
        if (selected == null) {
            showError(AppLocale.get("error.no.selection"))
            return
        }
        if (!Session.isOwnObject(selected.ownerId.value)) {
            showError(AppLocale.get("error.not.owner"))
            return
        }
        val dialog = EditDialog(stage.scene.window, selected.toHumanBeing())
        val humanBeing = dialog.showAndWait() ?: return
        val id = selected.id.value ?: return
        controller.executeCommand(name = "update", args = listOf(id.toString()), humanBeing = humanBeing) { success, message ->
            if (success) controller.requestRefresh() else showError(message)
        }
    }

    private fun onDelete() {
        val selected = tableView.selectionModel.selectedItem ?: return
        if (!Session.isOwnObject(selected.ownerId.value)) {
            showError(AppLocale.get("error.not.owner"))
            return
        }
        val confirm = Alert(Alert.AlertType.CONFIRMATION)
        confirm.title = AppLocale.get("delete.confirm.title")
        confirm.headerText = AppLocale.get("delete.confirm")
        confirm.showAndWait().ifPresent { result ->
            if (result == ButtonType.OK) {
                controller.deleteObject(selected) { success, message ->
                    if (success) controller.requestRefresh() else showError(message)
                }
            }
        }
    }

    private fun showError(message: String) {
        val alert = Alert(Alert.AlertType.ERROR, message, ButtonType.OK)
        alert.show()
    }

    private class LocaleDisplay(val displayName: String, val locale: java.util.Locale) {
        override fun toString() = displayName
    }
}
=== ./gui-client/src/main/kotlin/view/AuthView.kt ===
package view

import controller.AuthController
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.layout.HBox
import javafx.scene.layout.VBox
import javafx.stage.Stage
import localization.AppLocale

class AuthView(private val stage: Stage, private val onSuccess: (() -> Unit)? = null) {

    private val controller = AuthController()
    private val hostField = TextField("localhost")
    private val portField = TextField("8080")
    private val usernameField = TextField()
    private val passwordField = PasswordField()
    private val loginToggle = ToggleButton()
    private val registerToggle = ToggleButton()
    private val submitButton = Button()
    private val errorLabel = Label()
    private val localeCombo = ComboBox<LocaleDisplay>()

    fun show() {
        updateTitle()
        stage.scene = Scene(buildUI(), 400.0, 440.0)
        stage.isResizable = false
        stage.show()
    }

    private fun buildUI(): VBox {
        val toggleGroup = ToggleGroup()
        loginToggle.toggleGroup = toggleGroup
        registerToggle.toggleGroup = toggleGroup
        loginToggle.isSelected = true

        AppLocale.bindLabel(loginToggle, "auth.login")
        AppLocale.bindLabel(registerToggle, "auth.register")

        val toggleBox = HBox(10.0, loginToggle, registerToggle)

        val usernameLabel = Label()
        AppLocale.bindLabel(usernameLabel, "auth.username")
        AppLocale.bindPrompt(usernameField, "auth.username")
        val usernameBox = VBox(3.0, usernameLabel, usernameField)

        val passwordLabel = Label()
        AppLocale.bindLabel(passwordLabel, "auth.password")
        AppLocale.bindPrompt(passwordField, "auth.password")
        val passwordBox = VBox(3.0, passwordLabel, passwordField)

        hostField.promptText = "localhost"
        val hostBox = VBox(3.0, Label().also { AppLocale.bindLabel(it, "auth.host") }, hostField)

        portField.promptText = "8080"
        val portBox = VBox(3.0, Label().also { AppLocale.bindLabel(it, "auth.port") }, portField)

        val serverBox = HBox(8.0, hostBox, portBox)

        AppLocale.availableLocales().forEach { loc ->
            localeCombo.items.add(LocaleDisplay(AppLocale.localeDisplayName(loc), loc))
        }
        localeCombo.selectionModel.selectFirst()
        localeCombo.setOnAction {
            val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
            AppLocale.currentLocale.set(selected.locale)
            updateTitle()
        }

        val localeLabel = Label()
        AppLocale.bindLabel(localeLabel, "main.locale")
        val localeBox = HBox(10.0, localeLabel, localeCombo)

        submitButton.setOnAction { onLogin() }
        bindSubmitText()

        toggleGroup.selectedToggleProperty().addListener { _, _, _ -> bindSubmitText() }

        errorLabel.style = "-fx-text-fill: red;"

        val form = VBox(12.0,
            toggleBox,
            usernameBox,
            passwordBox,
            serverBox,
            localeBox,
            submitButton,
            errorLabel,
        )
        form.alignment = Pos.CENTER
        form.padding = Insets(30.0)

        return form
    }

    private fun bindSubmitText() {
        val key = if (loginToggle.isSelected) "auth.submit.login" else "auth.submit.register"
        AppLocale.bind(submitButton.textProperty(), key)
    }

    private fun updateTitle() {
        stage.title = AppLocale.get("auth.title")
    }

    private fun onLogin() {
        val host = hostField.text.trim()
        val portText = portField.text.trim()
        val username = usernameField.text.trim()
        val password = passwordField.text.trim()

        if (host.isEmpty() || portText.isEmpty() || username.isEmpty() || password.isEmpty()) {
            errorLabel.text = AppLocale.get("auth.error.empty")
            return
        }

        val port = portText.toIntOrNull()
        if (port == null || port !in 1..65535) {
            errorLabel.text = AppLocale.get("auth.error.invalid.port")
            return
        }

        val isLogin = loginToggle.isSelected
        submitButton.isDisable = true
        errorLabel.text = ""

        controller.authenticate(host, port, username, password, isLogin) { success, message ->
            submitButton.isDisable = false
            if (success) {
                stage.close()
                onSuccess?.invoke()
            } else {
                errorLabel.text = message
            }
        }
    }

    private class LocaleDisplay(val displayName: String, val locale: java.util.Locale) {
        override fun toString() = displayName
    }
}
=== ./gui-client/src/main/kotlin/localization/AppLocale.kt ===
package localization

import javafx.beans.property.ObjectProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.scene.control.Labeled
import java.util.Locale

object AppLocale {

    val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))

    private val strings: Map<Locale, Map<String, String>> = mapOf(
        Locale("ru") to mapOf(
            "auth.title" to "Вход / Регистрация",
            "auth.login" to "Вход",
            "auth.register" to "Регистрация",
            "auth.username" to "Имя пользователя",
            "auth.password" to "Пароль",
            "auth.host" to "Сервер",
            "auth.port" to "Порт",
            "auth.error.invalid.port" to "Некорректный порт (1–65535)",
            "auth.submit.login" to "Войти",
            "auth.submit.register" to "Зарегистрироваться",
            "auth.error.empty" to "Заполните все поля",
            "auth.error.failed" to "Ошибка входа. Проверьте логин и пароль.",
            "auth.error.register.failed" to "Ошибка регистрации. Возможно, пользователь уже существует.",
            "auth.error.server" to "Сервер недоступен",
            "auth.success.login" to "Успешный вход",
            "auth.success.register" to "Успешная регистрация",
            "main.title" to "Коллекция HumanBeing",
            "main.user" to "Пользователь",
            "main.logout" to "Выйти",
            "main.locale" to "Язык",
            "main.filter" to "Фильтр",
            "visualization.title" to "Визуализация",
            "table.name" to "Имя",
            "table.coordinates.x" to "X",
            "table.coordinates.y" to "Y",
            "table.creationDate" to "Дата создания",
            "table.realHero" to "Герой",
            "table.hasToothpick" to "Зубочистка",
            "table.impactSpeed" to "Скорость",
            "table.soundtrackName" to "Саундтрек",
            "table.minutesOfWaiting" to "Минуты ожидания",
            "table.weaponType" to "Оружие",
            "table.car.cool" to "Крутая тачка",
            "table.owner" to "Владелец",
            "button.add" to "Добавить",
            "button.edit" to "Редактировать",
            "button.delete" to "Удалить",
            "button.refresh" to "Обновить",
            "dialog.title.add" to "Добавление объекта",
            "dialog.title.edit" to "Редактирование объекта",
            "dialog.ok" to "OK",
            "dialog.cancel" to "Отмена",
            "dialog.error.validation" to "Ошибка валидации",
            "dialog.error.name" to "Имя не может быть пустым",
            "dialog.error.number" to "Некорректное число",
            "dialog.error.no.user" to "Ошибка: пользователь не идентифицирован",
            "info.title" to "Информация об объекте",
            "info.owner" to "Владелец",
            "info.id" to "ID",
            "delete.confirm" to "Удалить этот объект?",
            "delete.confirm.title" to "Подтверждение удаления",
            "error.no.selection" to "Объект не выбран",
            "error.not.owner" to "Вы не владелец этого объекта",
        ),
        Locale("nl") to mapOf(
            "auth.title" to "Inloggen / Registreren",
            "auth.login" to "Inloggen",
            "auth.register" to "Registreren",
            "auth.username" to "Gebruikersnaam",
            "auth.password" to "Wachtwoord",
            "auth.host" to "Server",
            "auth.port" to "Poort",
            "auth.error.invalid.port" to "Ongeldige poort (1–65535)",
            "auth.submit.login" to "Inloggen",
            "auth.submit.register" to "Registreren",
            "auth.error.empty" to "Vul alle velden in",
            "auth.error.failed" to "Inloggen mislukt. Controleer uw gegevens.",
            "auth.error.register.failed" to "Registratie mislukt. Gebruiker bestaat mogelijk al.",
            "auth.error.server" to "Server niet bereikbaar",
            "auth.success.login" to "Succesvol ingelogd",
            "auth.success.register" to "Succesvol geregistreerd",
            "main.title" to "HumanBeing Collectie",
            "main.user" to "Gebruiker",
            "main.logout" to "Uitloggen",
            "main.locale" to "Taal",
            "main.filter" to "Filter",
            "visualization.title" to "Visualisatie",
            "table.name" to "Naam",
            "table.coordinates.x" to "X",
            "table.coordinates.y" to "Y",
            "table.creationDate" to "Aanmaakdatum",
            "table.realHero" to "Held",
            "table.hasToothpick" to "Tandenstoker",
            "table.impactSpeed" to "Snelheid",
            "table.soundtrackName" to "Soundtrack",
            "table.minutesOfWaiting" to "Wachtminuten",
            "table.weaponType" to "Wapen",
            "table.car.cool" to "Coole auto",
            "table.owner" to "Eigenaar",
            "button.add" to "Toevoegen",
            "button.edit" to "Bewerken",
            "button.delete" to "Verwijderen",
            "button.refresh" to "Vernieuwen",
            "dialog.title.add" to "Object toevoegen",
            "dialog.title.edit" to "Object bewerken",
            "dialog.ok" to "OK",
            "dialog.cancel" to "Annuleren",
            "dialog.error.validation" to "Validatiefout",
            "dialog.error.name" to "Naam mag niet leeg zijn",
            "dialog.error.number" to "Ongeldig nummer",
            "dialog.error.no.user" to "Fout: gebruiker niet geïdentificeerd",
            "info.title" to "Objectinformatie",
            "info.owner" to "Eigenaar",
            "info.id" to "ID",
            "delete.confirm" to "Dit object verwijderen?",
            "delete.confirm.title" to "Verwijderen bevestigen",
            "error.no.selection" to "Geen object geselecteerd",
            "error.not.owner" to "U bent niet de eigenaar van dit object",
        ),
        Locale("hu") to mapOf(
            "auth.title" to "Bejelentkezés / Regisztráció",
            "auth.login" to "Bejelentkezés",
            "auth.register" to "Regisztráció",
            "auth.username" to "Felhasználónév",
            "auth.password" to "Jelszó",
            "auth.host" to "Szerver",
            "auth.port" to "Port",
            "auth.error.invalid.port" to "Érvénytelen port (1–65535)",
            "auth.submit.login" to "Belépés",
            "auth.submit.register" to "Regisztrálás",
            "auth.error.empty" to "Töltse ki az összes mezőt",
            "auth.error.failed" to "Sikertelen bejelentkezés. Ellenőrizze adatait.",
            "auth.error.register.failed" to "Sikertelen regisztráció. Lehet, hogy a felhasználó már létezik.",
            "auth.error.server" to "Szerver nem elérhető",
            "auth.success.login" to "Sikeres bejelentkezés",
            "auth.success.register" to "Sikeres regisztráció",
            "main.title" to "HumanBeing Gyűjtemény",
            "main.user" to "Felhasználó",
            "main.logout" to "Kijelentkezés",
            "main.locale" to "Nyelv",
            "main.filter" to "Szűrő",
            "visualization.title" to "Vizualizáció",
            "table.name" to "Név",
            "table.coordinates.x" to "X",
            "table.coordinates.y" to "Y",
            "table.creationDate" to "Létrehozás dátuma",
            "table.realHero" to "Hős",
            "table.hasToothpick" to "Fogpiszkáló",
            "table.impactSpeed" to "Sebesség",
            "table.soundtrackName" to "Soundtrack",
            "table.minutesOfWaiting" to "Várakozási percek",
            "table.weaponType" to "Fegyver",
            "table.car.cool" to "Menő autó",
            "table.owner" to "Tulajdonos",
            "button.add" to "Hozzáadás",
            "button.edit" to "Szerkesztés",
            "button.delete" to "Törlés",
            "button.refresh" to "Frissítés",
            "dialog.title.add" to "Objektum hozzáadása",
            "dialog.title.edit" to "Objektum szerkesztése",
            "dialog.ok" to "OK",
            "dialog.cancel" to "Mégse",
            "dialog.error.validation" to "Érvényesítési hiba",
            "dialog.error.name" to "A név nem lehet üres",
            "dialog.error.number" to "Érvénytelen szám",
            "dialog.error.no.user" to "Hiba: a felhasználó nincs azonosítva",
            "info.title" to "Objektum információ",
            "info.owner" to "Tulajdonos",
            "info.id" to "ID",
            "delete.confirm" to "Törli ezt az objektumot?",
            "delete.confirm.title" to "Törlés megerősítése",
            "error.no.selection" to "Nincs objektum kiválasztva",
            "error.not.owner" to "Ön nem a tulajdonosa ennek az objektumnak",
        ),
        Locale("en", "AU") to mapOf(
            "auth.title" to "Sign In / Sign Up",
            "auth.login" to "Sign In",
            "auth.register" to "Sign Up",
            "auth.username" to "Username",
            "auth.password" to "Password",
            "auth.host" to "Server",
            "auth.port" to "Port",
            "auth.error.invalid.port" to "Invalid port (1–65535)",
            "auth.submit.login" to "Sign In",
            "auth.submit.register" to "Sign Up",
            "auth.error.empty" to "Fill in all fields, mate",
            "auth.error.failed" to "Login failed. Check ya credentials.",
            "auth.error.register.failed" to "Registration failed. Reckon that user already exists.",
            "auth.error.server" to "Server's cactus",
            "auth.success.login" to "G'day! Logged in successfully",
            "auth.success.register" to "Registered, bonza!",
            "main.title" to "HumanBeing Collection",
            "main.user" to "User",
            "main.logout" to "Logout",
            "main.locale" to "Language",
            "main.filter" to "Filter",
            "visualization.title" to "Visualisation",
            "table.name" to "Name",
            "table.coordinates.x" to "X",
            "table.coordinates.y" to "Y",
            "table.creationDate" to "Creation Date",
            "table.realHero" to "Hero",
            "table.hasToothpick" to "Toothpick",
            "table.impactSpeed" to "Impact Speed",
            "table.soundtrackName" to "Soundtrack",
            "table.minutesOfWaiting" to "Minutes of Waitin'",
            "table.weaponType" to "Weapon",
            "table.car.cool" to "Carn't",
            "table.owner" to "Owner",
            "button.add" to "Add",
            "button.edit" to "Edit",
            "button.delete" to "Delete",
            "button.refresh" to "Refresh",
            "dialog.title.add" to "Add Object",
            "dialog.title.edit" to "Edit Object",
            "dialog.ok" to "OK",
            "dialog.cancel" to "Cancel",
            "dialog.error.validation" to "Validation Error",
            "dialog.error.name" to "Name can't be empty",
            "dialog.error.number" to "Invalid number, mate",
            "dialog.error.no.user" to "Error: user not identified, mate",
            "info.title" to "Object Info",
            "info.owner" to "Owner",
            "info.id" to "ID",
            "delete.confirm" to "Reckon you wanna delete this?",
            "delete.confirm.title" to "Confirm Deletion",
            "error.no.selection" to "No object selected",
            "error.not.owner" to "You're not the owner of this object",
        ),
    )

    fun get(key: String): String {
        return strings[currentLocale.value]?.get(key) ?: key
    }

    fun bind(property: javafx.beans.property.Property<String>, key: String) {
        property.value = get(key)
        currentLocale.addListener { _, _, _ ->
            property.value = get(key)
        }
    }

    fun bindLabel(labeled: Labeled, key: String) {
        bind(labeled.textProperty(), key)
    }

    fun bindPrompt(textField: javafx.scene.control.TextInputControl, key: String) {
        bind(textField.promptTextProperty(), key)
    }

    fun availableLocales(): List<Locale> = strings.keys.toList()

    fun localeDisplayName(locale: Locale): String = when (locale) {
        Locale("ru") -> "Русский"
        Locale("nl") -> "Nederlands"
        Locale("hu") -> "Magyar"
        Locale("en", "AU") -> "English (Australia)"
        else -> locale.displayName
    }
}
=== ./server/src/main/kotlin/database/tables/CoordinatesTable.kt ===
package database.tables
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable

object CoordinatesTable : UUIDTable("coordinates") {
    val x = long("x").nullable()
    val y = integer("y").nullable()
}
=== ./server/src/main/kotlin/database/tables/HumanBeingTable.kt ===
package database.tables

import models.WeaponType
import org.jetbrains.exposed.v1.javatime.date
import org.postgresql.util.PGobject
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable


object HumanBeingTable : UUIDTable("humanBeing") {
    val owner_id            = reference("owner_id", UserTable.id)
    val name                = text("name")
    val coordinates         = reference("coordinates", CoordinatesTable.id)
    val creationDate        = date("creation_date")
    val realHero            = bool("real_hero")
    val hasToothpick        = bool("has_toothpick")
    val impactSpeed         = double("impact_speed")
    val soundtrackName      = text("soundtrack_name")
    val minutesOfWaiting    = float("minutes_of_waiting")
    val car                 = reference("car", CarTable.id)
    val weaponType          = customEnumeration(
        name = "weapon_type",
        sql = "weapontype",
        fromDb = { value ->
            val strValue = when (value) {
                is PGobject -> value.value!!
                is String   -> value
                else        -> error("Unexpected type: ${value::class}")
            }
            WeaponType.valueOf(strValue.uppercase())
        },
        toDb = { value ->
            PGobject().apply {
                type = "weapontype"
                this.value = value.name
            }
        }
    ).nullable().default(null)
}=== ./server/src/main/kotlin/database/tables/CarTable.kt ===
package database.tables

import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable

object CarTable : UUIDTable("car") {
    val cool = bool("cool").nullable()
}=== ./server/src/main/kotlin/database/tables/UserTable.kt ===
package database.tables

import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable

object UserTable: UUIDTable("user") {
    val username = text("username")
    val password = text("password") // change
    val token = text("fresh_token").nullable()
}=== ./server/src/main/kotlin/database/CacheInvalidator.kt ===
package database

import io.github.oshai.kotlinlogging.KotlinLogging
import org.postgresql.PGConnection
import javax.sql.DataSource

private val logger = KotlinLogging.logger {}

class CacheInvalidator(
    private val dataSource: DataSource,
    private val onInvalidated: () -> Unit
) {
    @Volatile
    private var running = false
    private var thread: Thread? = null

    fun start() {
        running = true
        thread = Thread {
            while (running) {
                try {
                    dataSource.connection.use { conn ->
                        conn.autoCommit = true
                        val pgConn = conn.unwrap(PGConnection::class.java)

                        conn.createStatement().use { stmt ->
                            stmt.execute("LISTEN cache_changed")
                        }

                        logger.info { "Listening for cache changes via PG NOTIFY..." }

                        while (running && !conn.isClosed) {
                            val notifications = pgConn.notifications
                            if (notifications != null && notifications.isNotEmpty()) {
                                logger.debug { "Cache invalidation received" }
                                onInvalidated()
                            }
                            Thread.sleep(500)
                        }
                    }
                } catch (e: Exception) {
                    if (!running) break
                    logger.warn(e) { "Cache listener error, reconnecting in 5s..." }
                    Thread.sleep(5000)
                }
            }
        }.also { it.isDaemon = true }.apply {
            name = "pg-notify-listener"
            start()
        }
    }

    fun stop() {
        running = false
        thread?.interrupt()
    }
}
=== ./server/src/main/kotlin/database/Repository.kt ===
package database

import auth.JWTManager
import database.tables.CarTable
import database.tables.CoordinatesTable
import database.tables.HumanBeingTable
import database.tables.UserTable
import io.github.oshai.kotlinlogging.KotlinLogging
import models.Car
import models.Coordinates
import models.HumanBeing
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import java.util.UUID

private val logger = KotlinLogging.logger {}

class Repository(
    private val jwtManager: JWTManager,
) {

    fun read(): List<HumanBeing> {
        val beings = mutableListOf<HumanBeing>()
        transaction {
            val humans = HumanBeingTable
                .selectAll()

            for (human in humans) {
                logger.trace { "read. $human" }
                val cord = CoordinatesTable
                    .select(CoordinatesTable.x, CoordinatesTable.y)
                    .where(CoordinatesTable.id eq human[HumanBeingTable.coordinates])
                    .single()

                val car = CarTable
                    .select(CarTable.cool)
                    .where(CarTable.id eq human[HumanBeingTable.car])
                    .single()

                val newCords = Coordinates(cord[CoordinatesTable.x]!!, cord[CoordinatesTable.y]!!)
                val newCar = Car(car[CarTable.cool]!!)
                val newBeing = HumanBeing(
                    name = human[HumanBeingTable.name],
                    ownerId = human[HumanBeingTable.owner_id].value,
                    coordinates = newCords,
                    car = newCar,
                    id = human[HumanBeingTable.id].value,
                    creationDate = human[HumanBeingTable.creationDate],
                    realHero = human[HumanBeingTable.realHero],
                    hasToothpick = human[HumanBeingTable.hasToothpick],
                    impactSpeed = human[HumanBeingTable.impactSpeed],
                    soundtrackName = human[HumanBeingTable.soundtrackName],
                    minutesOfWaiting = human[HumanBeingTable.minutesOfWaiting],
                    weaponType = human[HumanBeingTable.weaponType]
                )

                beings.add(newBeing)
            }
        }
        return beings
    }

    fun write(item: HumanBeing): UUID?  {
        var humanId: UUID? = null
        try {
            transaction {
                val cords = CoordinatesTable.insertAndGetId {
                    it[x] = item.coordinates.x
                    it[y] = item.coordinates.y
                }

                val cars = CarTable.insertAndGetId {
                    it[cool] = item.car.cool
                }

                humanId = HumanBeingTable.insertAndGetId {
                    it[owner_id] = item.ownerId
                    it[name] = item.name
                    it[coordinates] = cords
                    it[creationDate] = item.creationDate
                    it[realHero] = item.realHero
                    it[hasToothpick] = item.hasToothpick
                    it[impactSpeed] = item.impactSpeed
                    it[soundtrackName] = item.soundtrackName
                    it[minutesOfWaiting] = item.minutesOfWaiting
                    it[car] = cars
                    it[weaponType] = item.weaponType
                }.value
                exec("NOTIFY cache_changed")
            }

        } catch (e: Exception) {
            logger.error(e) { "write failed. Here what's going on: \n $e \n " }
            return humanId
        }
        return humanId
    }

    fun update(id: UUID, item: HumanBeing): Boolean {
        var allGood = false
        transaction {
            val check = HumanBeingTable
                .select(HumanBeingTable.id)
                .where(HumanBeingTable.id eq id)
                .singleOrNull()

            if (check != null) {
                val refs = HumanBeingTable
                    .select(HumanBeingTable.coordinates, HumanBeingTable.car)
                    .where(HumanBeingTable.id eq id)
                    .single()

                CoordinatesTable.update(
                    { CoordinatesTable.id eq refs[HumanBeingTable.coordinates] }
                ) {
                    it[x] = item.coordinates.x
                    it[y] = item.coordinates.y
                }

                CarTable.update(
                    { CarTable.id eq refs[HumanBeingTable.car] }
                ) {
                    it[cool] = item.car.cool
                }

                HumanBeingTable.update(
                    { HumanBeingTable.id eq id }
                ) {
                    it[owner_id] = item.ownerId
                    it[name] = item.name
                    it[creationDate] = item.creationDate
                    it[realHero] = item.realHero
                    it[impactSpeed] = item.impactSpeed
                    it[soundtrackName] = item.soundtrackName
                    it[minutesOfWaiting] = item.minutesOfWaiting
                    it[hasToothpick] = item.hasToothpick
                    it[weaponType] = item.weaponType
                }
                exec("NOTIFY cache_changed")
                allGood = true
            }
        }
        return allGood
    }

    fun delete(id: UUID): Boolean {
        var allGood = true
        try {
            transaction {
                val refs = HumanBeingTable
                    .select(HumanBeingTable.coordinates, HumanBeingTable.car)
                    .where { HumanBeingTable.id eq id }
                    .singleOrNull()

                if (refs != null) {
                    HumanBeingTable.deleteWhere { HumanBeingTable.id eq id }
                    CoordinatesTable.deleteWhere { CoordinatesTable.id eq refs[HumanBeingTable.coordinates] }
                    CarTable.deleteWhere { CarTable.id eq refs[HumanBeingTable.car] }
                    exec("NOTIFY cache_changed")
                } else {
                    allGood = false
                }
            }
        } catch (e: Exception) {
            allGood = false
        }
        return allGood
    }

    fun login(username: String, password: String): Pair<String, String>? {
        var id: String? = null
        transaction {
            id = UserTable.select(UserTable.id)
                .where { (UserTable.username eq username) and (UserTable.password eq password) }
                .singleOrNull()
                ?.get(UserTable.id)?.toString()
        }
        if (id == null) return null
        val newToken = jwtManager.createJWT(60, id!!)
        transaction {
            UserTable.update({ UserTable.id eq UUID.fromString(id) }) {
                it[UserTable.token] = newToken
            }
        }
        return Pair(newToken, id!!)
    }

    fun register(username: String, password: String): Pair<String, String>? {
        var id: String? = null
        transaction {
            val check = UserTable.select(UserTable.username)
                .where { UserTable.username eq username }
                .singleOrNull()
            if (check == null) {
                id = UserTable.insertAndGetId {
                    it[UserTable.username] = username
                    it[UserTable.password] = password
                }.value.toString()
            }
        }
        if (id == null) return null
        val newToken = jwtManager.createJWT(60, id!!)
        transaction {
            UserTable.update({ UserTable.id eq UUID.fromString(id) }) {
                it[UserTable.token] = newToken
            }
        }
        return Pair(newToken, id!!)
    }

    fun deleteByOwner(ownerId: UUID): Int {
        var deletedCount = 0
        transaction {
            val humanIds = HumanBeingTable
                .select(HumanBeingTable.id, HumanBeingTable.coordinates, HumanBeingTable.car)
                .where { HumanBeingTable.owner_id eq ownerId }
                .toList()
            val coordIds = humanIds.map { it[HumanBeingTable.coordinates] }
            val carIds = humanIds.map { it[HumanBeingTable.car] }
            deletedCount = HumanBeingTable.deleteWhere { HumanBeingTable.owner_id eq ownerId }
            CoordinatesTable.deleteWhere { CoordinatesTable.id inList coordIds }
            CarTable.deleteWhere         { CarTable.id         inList carIds    }
            exec("NOTIFY cache_changed")
        }
        return deletedCount
    }
}
=== ./server/src/main/kotlin/database/DatabaseManager.kt ===
package database

import com.zaxxer.hikari.*
import database.tables.CarTable
import database.tables.CoordinatesTable
import database.tables.HumanBeingTable
import database.tables.UserTable
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction

class DatabaseManager {
    lateinit var dataSource: HikariDataSource
        private set

    fun start() {
        val config = HikariConfig().apply {
            jdbcUrl = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/prog7_db"
            driverClassName = "org.postgresql.Driver"
            username = System.getenv("DB_USER") ?: error("DB_USER not set")
            password = System.getenv("DB_PASS") ?: error("DB_PASS not set")
            maximumPoolSize = 11
            isAutoCommit = false
            transactionIsolation = "TRANSACTION_REPEATABLE_READ"
            validate()
        }
        dataSource = HikariDataSource(config)
        Database.connect(dataSource)

        transaction {
            exec(
                """
                DO ${'$'}${'$'} BEGIN
                    CREATE TYPE weapontype AS ENUM ('HAMMER', 'SHOTGUN', 'BAT');
                EXCEPTION
                    WHEN duplicate_object THEN null;
                END ${'$'}${'$'};
            """
            )
            SchemaUtils.create(CoordinatesTable)
            SchemaUtils.create(CarTable)
            SchemaUtils.create(HumanBeingTable)
            SchemaUtils.create(UserTable)
        }
    }

    fun test(): List<String> {
        val smth = mutableListOf<String>()

        transaction {
            val rows = HumanBeingTable.selectAll()
            rows.forEach { row ->
                smth.add(row[HumanBeingTable.name])
            }
        }
        return smth
    }
}=== ./server/src/main/kotlin/collection/CollectionManager.kt ===
package collection

import models.HumanBeing
import mu.KotlinLogging
import java.time.LocalDate
import java.util.Collections
import java.util.TreeSet
import java.util.UUID

private val logger = KotlinLogging.logger {}

class CollectionManager {

    private val collection: MutableSet<HumanBeing> =
        Collections.synchronizedSortedSet(TreeSet())

    val initDate: LocalDate = LocalDate.now()

    fun add(humanBeing: HumanBeing): Boolean {
        logger.info { "[ADD] Element created ${humanBeing.id}" }
        return collection.add(humanBeing)
    }

    fun update(id: UUID, updated: HumanBeing): Boolean {
        synchronized(collection) {
            val old = getById(id) ?: return false
            collection.remove(old)
            val replaced = updated.copy(id = old.id, creationDate = old.creationDate)
            logger.info { "[UPDATE] Element updated ${old.id}" }
            return collection.add(replaced)
        }
    }

    fun removeById(id: UUID): Boolean {
        val target = getById(id) ?: return false
        logger.info { "[REMOVE] Element deleted ${target.id}" }
        return collection.remove(target)
    }

    fun clear() {
        collection.clear()
        logger.info { "[CLEAR] Collection cleared" }
    }

    fun getAll(): TreeSet<HumanBeing> {
        synchronized(collection) {
            return TreeSet(collection)
        }
    }

    fun getById(id: UUID): HumanBeing? {
        synchronized(collection) {
            return collection.firstOrNull { it.id == id }
        }
    }

    fun size(): Int = collection.size

    fun isEmpty(): Boolean = collection.isEmpty()

    fun getMax(): HumanBeing? {
        synchronized(collection) {
            return collection.maxWithOrNull(Comparator.naturalOrder())
        }
    }

    fun getMin(): HumanBeing? {
        synchronized(collection) {
            return collection.minWithOrNull(Comparator.naturalOrder())
        }
    }

    fun sumOfMinutesOfWaiting(): Double {
        synchronized(collection) {
            return collection.sumOf { it.minutesOfWaiting.toDouble() }
        }
    }

    fun minByName(): HumanBeing? {
        synchronized(collection) {
            return collection.minByOrNull { it.name }
        }
    }

    fun getMinutesOfWaitingDescending(): List<Float> {
        synchronized(collection) {
            return collection.map { it.minutesOfWaiting }.sortedDescending()
        }
    }

    fun loadFromFile(items: List<HumanBeing>) {
        synchronized(collection) {
            collection.clear()
            collection.addAll(items)
        }
        logger.info { "[LOAD] loaded ${items.size} elements" }
    }

    fun getInfo(): String =
        """
         |Тип коллекции : ${collection::class.simpleName}
         |Тип элементов : ${HumanBeing::class.simpleName}
         |Дата инициал. : $initDate
         |Кол-во элемен.: ${collection.size}
         """.trimMargin()
}
=== ./server/src/main/kotlin/auth/JWTManager.kt ===
package auth

import io.fusionauth.jwt.JWTExpiredException
import io.fusionauth.jwt.JWTUnavailableForProcessingException
import io.fusionauth.jwt.Signer
import io.fusionauth.jwt.Verifier
import io.fusionauth.jwt.domain.JWT
import io.fusionauth.jwt.hmac.HMACSigner
import io.fusionauth.jwt.hmac.HMACVerifier
import java.time.ZoneOffset
import java.time.ZonedDateTime

class JWTManager (
    private val jwtSecret: String = System.getenv("JWT_SECRET") ?: error("JWT_SECRET not set"),
    private val issuer: String = "aklivtsov.tech",
) {
    private val signer: Signer = HMACSigner.newSHA256Signer(jwtSecret)
    private val verifier: Verifier = HMACVerifier.newVerifier(jwtSecret)

    fun createJWT(expiredAfterMinutes: Long, userId: String): String {
        val jwt: JWT = JWT()
            .setIssuer(issuer)
            .setIssuedAt(ZonedDateTime.now(ZoneOffset.UTC))
            .setSubject(userId)
            .setExpiration(ZonedDateTime.now(ZoneOffset.UTC).plusMinutes(expiredAfterMinutes))

        return JWT.getEncoder().encode(jwt, signer)
    }

    fun getUserId(jwt: String): String? {
        return try {
            JWT.getDecoder().decode(jwt, verifier).subject
        } catch (e: Exception) {
            null
        }
    }

    fun validateJWT(jwt: String, userId: String): Boolean {
        return try {
            JWT.getDecoder().decode(jwt, verifier)
            true
        } catch (e: JWTExpiredException) {
            false
        } catch (e: JWTUnavailableForProcessingException) {
            false
        } catch (e: Exception) {
            false
        }
    }
}=== ./server/src/main/kotlin/runner/CommandInvoker.kt ===
package runner

import auth.JWTManager
import commands.Command
import mu.KotlinLogging
import network.Request
import network.Response

/**
 * Серверный инвокер.
 *
 * Принимает Request, находит команду, выполняет,
 * возвращает Response готовый для отправки клиенту.
 */

private val logger = KotlinLogging.logger {}

class CommandInvoker(
    private val jwtManager: JWTManager
) {
    private val commands = mutableMapOf<String, Command>()
    private val history = ArrayDeque<String>()

    fun register(command: Command) {
        commands[command.name] = command
    }

    fun execute(request: Request): Response {

        val command = commands[request.commandName]
            ?: return Response(false, "Неизвестная команда: '${request.commandName}'")

        if (request.jwt == "no token")
            return Response(false, "Нет токена. Войдите или зарегистрируйтесь")

        if (!jwtManager.validateJWT(request.jwt, request.jwt))
            return Response(false, "Время истекло. Выполните вход заново.")

        val userId = jwtManager.getUserId(request.jwt)
            ?: return Response(false, "Не удалось определить пользователя")

        addToHistory(request.commandName)

        return try {
            command.execute(request.args, request.humanBeing, userId)
        } catch (e: Exception) {
            Response(false, "[Ошибка выполнения] ${e.message}")
        }
    }

    fun getHistory(): List<String> = history.toList()
    fun getCommands(): Map<String, Command> = commands.toMap()

    private fun addToHistory(name: String) {
        if (history.size >= 12) history.removeFirst()
        history.addLast(name)
    }
}
=== ./server/src/main/kotlin/network/NetworkManager.kt ===
package network

import database.Repository
import kotlinx.serialization.SerializationException
import mu.KotlinLogging
import runner.CommandInvoker
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.util.concurrent.Executors
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.RecursiveAction
import java.util.concurrent.RecursiveTask

private val logger = KotlinLogging.logger {}

class NetworkManager(
    private val port: Int,
    private val invoker: CommandInvoker,
    private val repository: Repository
) {
    private val socket = DatagramSocket(port)
    private val readerPool = Executors.newFixedThreadPool(4)
    private val processingPool = ForkJoinPool(Runtime.getRuntime().availableProcessors())
    private val senderPool = ForkJoinPool(Runtime.getRuntime().availableProcessors())

    fun start() {
        logger.info { "Server started on port $port. Waiting for connection..." }
        repeat(4) {
            readerPool.submit(::readLoop)
        }
        try {
            Thread.currentThread().join()
        } catch (_: InterruptedException) {}
    }

    private fun readLoop() {
        val buf = ByteArray(65507)
        while (!socket.isClosed) {
            try {
                val packet = DatagramPacket(buf.copyOf(), buf.size)
                socket.receive(packet)

                val address = packet.address
                val clientPort = packet.port
                val json = String(packet.data, 0, packet.length, Charsets.UTF_8)

                processingPool.submit(ProcessRequestTask(json, address, clientPort))

            } catch (e: Exception) {
                if (!socket.isClosed) logger.warn(e) { "Read error: ${e.message}" }
                else break
            }
        }
    }

    inner class ProcessRequestTask(
        private val json: String,
        private val address: InetAddress,
        private val clientPort: Int
    ) : RecursiveTask<Unit>() {

        override fun compute() {
            val response: Any = try {
                val authRequest = try {
                    AppJson.decodeFromString<AuthRequest>(json)
                } catch (_: SerializationException) { null }

                if (authRequest != null) {
                    handleAuth(authRequest)
                } else {
                    val request = try {
                        AppJson.decodeFromString<Request>(json)
                    } catch (e: SerializationException) {
                        logger.warn(e) { "Invalid request: ${e.message}" }
                        senderPool.submit(
                            SendResponseTask(
                                Response(false, "Некорректный формат запроса"),
                                address, clientPort
                            )
                        )
                        return
                    }
                    logger.info { "'${request.commandName}' from $address:$clientPort." }
                    invoker.execute(request)
                }
            } catch (e: Exception) {
                Response(false, "Внутренняя ошибка сервера: ${e.message}")
            }

            senderPool.submit(SendResponseTask(response, address, clientPort))
        }

        private fun handleAuth(authRequest: AuthRequest): AuthResponse {
            val res = if (authRequest.type == AuthType.LOGIN)
                repository.login(authRequest.login, authRequest.password)
            else
                repository.register(authRequest.login, authRequest.password)

            return if (res != null)
                AuthResponse(true, "success", res.first, res.second)
            else
                AuthResponse(false, "login failed", null, null)
        }
    }

    inner class SendResponseTask(
        private val response: Any,
        private val address: InetAddress,
        private val clientPort: Int
    ) : RecursiveAction() {

        override fun compute() {
            try {
                val bytes = when (response) {
                    is Response     -> AppJson.encodeToString(Response.serializer(), response)
                    is AuthResponse -> AppJson.encodeToString(AuthResponse.serializer(), response)
                    else -> return
                }.toByteArray(Charsets.UTF_8)

                val responsePacket = DatagramPacket(bytes, bytes.size, address, clientPort)
                synchronized(socket) {
                    socket.send(responsePacket)
                }
            } catch (e: Exception) {
                logger.warn(e) { "Send error: ${e.message}" }
            }
        }
    }

    fun stop() {
        logger.info { "Shutting down..." }
        socket.close()
        readerPool.shutdownNow()
        processingPool.shutdown()
        senderPool.shutdown()
    }
}
=== ./server/src/main/kotlin/Main.kt ===
import auth.JWTManager
import collection.CollectionManager
import commands.*
import database.CacheInvalidator
import database.DatabaseManager
import database.Repository
import mu.KotlinLogging
import network.NetworkManager
import runner.CommandInvoker
import java.io.File
import kotlin.system.exitProcess

private val logger = KotlinLogging.logger {}

fun main(args: Array<String>) {
    System.setProperty("slf4j.internal.verbosity", "WARN")
    loadDotEnv()
    val port = if (args.isNotEmpty() && args.size > 1) args[1].toInt() else 8080

    logger.info{ "[startup] Starting server on port $port with $args args" }

    val jwtManager = JWTManager()
    val database = DatabaseManager()
    database.start()
    val repository = Repository(jwtManager)

    val manager = CollectionManager()
    manager.loadFromFile(repository.read())

    // Синхронизация кэша между серверами через PG NOTIFY
    val cacheInvalidator = CacheInvalidator(database.dataSource) {
        manager.loadFromFile(repository.read())
    }
    cacheInvalidator.start()

    val invoker = CommandInvoker(jwtManager)

    registerServerCommands(invoker, manager, repository)

    // (Ctrl+C or kill)
    val server = NetworkManager(port = port, invoker = invoker, repository = repository)
    Runtime.getRuntime().addShutdownHook(Thread {
        logger.info{ "Exit..." }
        cacheInvalidator.stop()
        server.stop()
    })

    // Server-side console
    Thread {
        val scanner = java.util.Scanner(System.`in`)
        logger.info{ "[Server-console] Server-side commands: exit" }
        while (scanner.hasNextLine()) {
            when (scanner.nextLine().trim().lowercase()) {
                "exit" -> {
                    logger.info{ "[Server-console] Shutting down..." }
                    exitProcess(0)
                }
                else -> logger.info{ "[Server-console] Server-side commands: exit" }
            }
        }
    }.also { it.isDaemon = true }.start()

    server.start()
}

private fun loadDotEnv() {
    val envFile = File(".env")
    if (!envFile.exists()) {
        logger.warn { ".env not found, using system env vars" }
        return
    }
    envFile.forEachLine { line ->
        val trimmed = line.trim()
        if (trimmed.isEmpty() || trimmed.startsWith("#")) return@forEachLine
        val idx = trimmed.indexOf('=')
        if (idx == -1) return@forEachLine
        val key = trimmed.substring(0, idx).trim()
        val value = trimmed.substring(idx + 1).trim().removeSurrounding("\"").removeSurrounding("'")
        setEnv(key, value)
    }
}

@Suppress("UNCHECKED_CAST")
private fun setEnv(key: String, value: String) {
    try {
        val processEnv = System.getenv()::class.java
        val field = processEnv.getDeclaredField("m")
        field.isAccessible = true
        (field.get(System.getenv()) as MutableMap<String, String>)[key] = value
    } catch (_: Exception) {
        System.setProperty(key, value)
    }
}

fun registerServerCommands(
    invoker: CommandInvoker,
    manager: CollectionManager,
    repository: Repository
) {
    listOf(
        InfoCommand(manager),
        ShowCommand(manager),
        AddCommand(manager, repository),
        UpdateCommand(manager, repository),
        RemoveByIdCommand(manager, repository),
        ClearCommand(manager, repository),
        AddIfMaxCommand(manager, repository),
        AddIfMinCommand(manager, repository),
        HistoryCommand(invoker),
        SyncCommand(invoker),
        SumOfMinutesCommand(manager),
        MinByNameCommand(manager),
        PrintDescendingMinutesCommand(manager),
    ).forEach { invoker.register(it)}
    logger.info{ " [startup] Registered commands: ${invoker.getCommands().keys}" }
}
=== ./server/src/main/kotlin/commands/HistoryCommand.kt ===
package commands

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import runner.CommandInvoker
import models.HumanBeing
import network.Response

@Serializable
class HistoryCommand(@Contextual private val invoker: CommandInvoker) : Command {
    override val name = "history"
    override val description = "вывести последние 12 команд"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        val history = invoker.getHistory()
        if (history.isEmpty()) return Response(true, "История пуста.")
        return Response(true, history.mapIndexed { i, cmd -> "  ${i + 1}. $cmd" }.joinToString("\n"))
    }
}
=== ./server/src/main/kotlin/commands/MinByNameCommand.kt ===
package commands

import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response

/**
 * Выводит любой элемент с минимальным значением поля [HumanBeing.name].
 */
@Serializable
class MinByNameCommand(@Contextual private val manager: CollectionManager) : Command {
    override val name = "min_by_name"
    override val description = "вывести элемент с минимальным именем"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response =
        Response(true, manager.minByName()?.toString() ?: "Коллекция пуста.")
}
=== ./server/src/main/kotlin/commands/AddIfMaxCommand.kt ===
package commands

import collection.CollectionManager
import database.Repository
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
import java.util.UUID

@Serializable
class AddIfMaxCommand(
    @Contextual val manager: CollectionManager,
    @Contextual private val repository: Repository
) : Command {
    override val name = "add_if_max"
    override val description = "добавить элемент, если он больше максимального"
    override val type = CommandType.MODEL
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        humanBeing ?: return Response(false, "[Ошибка] Объект HumanBeing не передан.")
        val secured = humanBeing.copy(ownerId = UUID.fromString(userId))
        val max = manager.getMax()
        return if (max == null || secured > max) {
            secured.id = repository.write(secured)
            secured.id ?: return Response(false, "Не удалось добавить элемент")
            manager.add(secured)
            Response(true, "Элемент добавлен (превышает максимум).")
        } else {
            Response(false, "Элемент не добавлен: не превышает текущий максимум (${max.name}).")
        }
    }
}
=== ./server/src/main/kotlin/commands/AddCommand.kt ===
package commands

import collection.CollectionManager
import database.Repository
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
import java.util.UUID

@Serializable
class AddCommand(
    @Contextual private val manager: CollectionManager,
    @Contextual private val repository: Repository
) : Command {
    override val name = "add"
    override val description = "добавить новый элемент в коллекцию"
    override val args = listOf(CommandArgType.NONE)
    override val type = CommandType.MODEL

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        humanBeing ?: return Response(false, "[Ошибка] Объект HumanBeing не передан.")
        val secured = humanBeing.copy(ownerId = UUID.fromString(userId))
        secured.id = repository.write(secured)
        secured.id ?: return Response(false, "Не удалось добавить элемент")

        return if (manager.add(secured))
            Response(true, "Элемент добавлен: ${secured.name} [${secured.id}]")

        else
            Response(false, "Элемент уже существует в коллекции.")
    }
}
=== ./server/src/main/kotlin/commands/AddIfMinCommand.kt ===
package commands

import collection.CollectionManager
import database.Repository
import models.HumanBeing
import kotlinx.serialization.Serializable
import kotlinx.serialization.Contextual
import network.Response
import java.util.UUID

@Serializable
class AddIfMinCommand(
    @Contextual private val manager: CollectionManager,
    @Contextual private val repository: Repository
) : Command {
    override val name = "add_if_min"
    override val description = "добавить элемент, если он меньше минимального"
    override val type = CommandType.MODEL
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        humanBeing ?: return Response(false, "[Ошибка] Объект HumanBeing не передан.")
        val secured = humanBeing.copy(ownerId = UUID.fromString(userId))
        val min = manager.getMin()
        return if (min == null || secured < min) {
            secured.id = repository.write(secured)
            secured.id ?: return Response(false, "Не удалось добавить элемент")
            manager.add(secured)
            Response(true, "Элемент добавлен (меньше минимума).")
        } else {
            Response(false, "Элемент не добавлен: не меньше текущего минимума (${min.name}).")
        }
    }
}
=== ./server/src/main/kotlin/commands/PrintDescendingMinutesCommand.kt ===
package commands

import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response

/**
 * Выводит значения поля [models.HumanBeing.minutesOfWaiting] в порядке убывания.
 */
@Serializable
class PrintDescendingMinutesCommand(@Contextual private val manager: CollectionManager) : Command {
    override val name = "print_field_descending_minutes_of_waiting"
    override val description = "вывести minutesOfWaiting в порядке убывания"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        val values = manager.getMinutesOfWaitingDescending()
        return if (values.isEmpty()) Response(true, "Коллекция пуста.")
        else Response(true, values.joinToString("\n"))
    }
}
=== ./server/src/main/kotlin/commands/UpdateCommand.kt ===
package commands

import collection.CollectionManager
import database.Repository
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
import java.util.UUID

@Serializable
class UpdateCommand(
    @Contextual private val manager: CollectionManager,
    @Contextual private val repository: Repository
) : Command {
    override val name = "update"
    override val description = "обновить элемент по id: update <uuid>"
    override val type = CommandType.MODEL
    override val args = listOf(CommandArgType.STRING)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        if (args.isEmpty()) return Response(false, "[Ошибка] Укажите id. Пример: update <UUID>")
        humanBeing ?: return Response(false, "[Ошибка] Объект HumanBeing не передан.")

        val id = try {
            UUID.fromString(args[0])
        } catch (e: IllegalArgumentException) {
            return Response(false, "[Ошибка] Некорректный UUID: '${args[0]}'")
        }

        val existing = manager.getById(id)
            ?: return Response(false, "Элемент не найден.")

        if (existing.ownerId.toString() != userId)
            return Response(false, "Нет прав: объект принадлежит другому пользователю.")

        if (repository.update(id, humanBeing)) {
            manager.update(id, humanBeing)
            return Response(true, "Элемент обновлён.")
        } else {
            return Response(false, "[Ошибка] Элемент не найден и не был обновлён.")
        }
    }
}
=== ./server/src/main/kotlin/commands/InfoCommand.kt ===
package commands

import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response

@Serializable
class InfoCommand(@Contextual private val manager: CollectionManager) : Command {
    override val name = "info"
    override val description = "вывести информацию о коллекции"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response =
        Response(true, manager.getInfo())
}
=== ./server/src/main/kotlin/commands/ClearCommand.kt ===
package commands

import collection.CollectionManager
import database.Repository
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
import java.util.UUID

@Serializable
class ClearCommand(
    @Contextual private val manager: CollectionManager,
    @Contextual private var repository: Repository
) : Command {
    override val name = "clear"
    override val description = "очистить коллекцию"
    override val args = listOf(CommandArgType.NONE)
    override val type = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        if (userId.isBlank()) return Response(false, "Не удалось определить пользователя.")
        val ownerId = UUID.fromString(userId)

        val deletedCount = repository.deleteByOwner(ownerId)
        val toRemove = manager.getAll().filter { it.ownerId == ownerId }
        toRemove.forEach { manager.removeById(it.id!!) }

        return Response(true, "Удалено объектов: $deletedCount.")
    }
}
=== ./server/src/main/kotlin/commands/RemoveByIdCommand.kt ===
package commands

import collection.CollectionManager
import database.Repository
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.UUID
import models.HumanBeing
import network.Response

@Serializable
class RemoveByIdCommand(
    @Contextual private val manager: CollectionManager,
    @Contextual private val repository: Repository
) : Command {
    override val name = "remove_by_id"
    override val description = "удалить элемент по id: remove_by_id <uuid>"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.STRING)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        if (args.isEmpty()) return Response(false, "[Ошибка] Укажите id. Пример: remove_by_id <uuid>")

        val id = try {
            UUID.fromString(args[0])
        } catch (e: IllegalArgumentException) {
            return Response(false, "[Ошибка] Некорректный UUID: '${args[0]}'")
        }

        val existing = manager.getById(id)
            ?: return Response(false, "Элемент не найден.")

        if (existing.ownerId.toString() != userId)
            return Response(false, "Нет прав: объект принадлежит другому пользователю.")

        if (repository.delete(id)) {
            manager.removeById(id)
            return Response(true, "Элемент удалён.")
        } else {
            return Response(false, "[Ошибка] Элемент с id '$id' не найден и не был удалён.")
        }
    }
}
=== ./server/src/main/kotlin/commands/ShowCommand.kt ===
package commands

import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response

@Serializable
class ShowCommand(@Contextual private val manager: CollectionManager) : Command {
    override val name = "show"
    override val description = "вывести все элементы коллекции"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
        if (manager.isEmpty()) return Response(true, "Коллекция пуста.")
        return Response(true, manager.getAll().joinToString("\n") { it.toString() })
    }
}
=== ./server/src/main/kotlin/commands/SumOfMinutesCommand.kt ===
package commands

import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response

/**
 * Выводит сумму значений поля [models.HumanBeing.minutesOfWaiting].
 */
@Serializable
class SumOfMinutesCommand(@Contextual private val manager: CollectionManager) : Command {
    override val name = "sum_of_minutes_of_waiting"
    override val description = "вывести сумму minutesOfWaiting всех элементов"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response =
        Response(true, "Сумма minutesOfWaiting: ${manager.sumOfMinutesOfWaiting()}")
}
=== ./server/src/main/kotlin/commands/SyncCommand.kt ===
package commands

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.CommandInfo
import network.Response
import runner.CommandInvoker

@Serializable
class SyncCommand(
    @Contextual private val invoker: CommandInvoker
) : Command {
    override val name: String = "sync"
    override val description: String = "Получает все доступные с сервера команды"
    override val type = CommandType.SIMPLE
    override val args = listOf(CommandArgType.NONE)

    override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {

        val commands = mutableMapOf<String, Command>()

        for (command in invoker.getCommands().values) {
            if (command.name == "sync") {
                continue
            }
                commands[command.name] = command
        }

        val commandInfos = commands.values.map { command ->
            CommandInfo(
                name = command.name,
                description = command.description,
                type = command.type,
                args = command.args
            )
        }

        return Response(
            success = true,
            message = "хуй",
            commands = commandInfos
        )
    }
}
=== ./common/src/main/kotlin/network/AuthRequest.kt ===
package network

import kotlinx.serialization.Serializable

@Serializable
data class AuthRequest(
    val login: String,
    val password: String,
    val type: AuthType,
)
=== ./common/src/main/kotlin/network/CommandInfo.kt ===
package network

import commands.CommandArgType
import commands.CommandType
import kotlinx.serialization.Serializable

@Serializable
data class CommandInfo(
    val name: String,
    val description: String,
    val args: List<CommandArgType>,
    val type: CommandType
)
=== ./common/src/main/kotlin/network/AuthType.kt ===
package network

import kotlinx.serialization.Serializable

@Serializable
enum class AuthType {
    LOGIN,
    REGISTER
}=== ./common/src/main/kotlin/network/utils/UUIDSerializer.kt ===
package network.utils

import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import java.util.UUID

/**
 * Кастомный KSerializer для UUID.
 * UUID не поддерживается kotlinx.serialization из коробки.
 */
object UUIDSerializer : KSerializer<UUID> {
    override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: UUID) =
        encoder.encodeString(value.toString())

    override fun deserialize(decoder: Decoder): UUID =
        UUID.fromString(decoder.decodeString())
}
=== ./common/src/main/kotlin/network/utils/LocalDateSerializer.kt ===
package network.utils

import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import java.time.LocalDate

/**
 * Кастомный KSerializer для LocalDate.
 * Сериализует как ISO строку: "2025-04-12"
 */
object LocalDateSerializer : KSerializer<LocalDate> {
    override val descriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: LocalDate) =
        encoder.encodeString(value.toString())

    override fun deserialize(decoder: Decoder): LocalDate =
        LocalDate.parse(decoder.decodeString())
}
=== ./common/src/main/kotlin/network/Response.kt ===
package network

import kotlinx.serialization.Serializable

@Serializable
data class Response(
    val success: Boolean,
    val message: String,
    val commands: List<CommandInfo>? = null,
)
=== ./common/src/main/kotlin/network/Request.kt ===
package network

import kotlinx.serialization.Serializable
import models.HumanBeing

@Serializable
data class Request(
    val commandName: String,
    val args: List<String> = emptyList(),
    val humanBeing: HumanBeing? = null,
    val jwt: String
)
=== ./common/src/main/kotlin/network/AuthResponse.kt ===
package network

import kotlinx.serialization.Serializable

@Serializable
data class AuthResponse (
    val success: Boolean,
    val message: String,
    val accessToken: String?,
    val id: String?
)=== ./common/src/main/kotlin/network/AppJson.kt ===
package network

import kotlinx.serialization.json.Json

val AppJson = Json {
    ignoreUnknownKeys = true
    encodeDefaults = true
}
=== ./common/src/main/kotlin/models/Car.kt ===
package models

import kotlinx.serialization.Serializable

/**
 * Автомобиль, которым владеет [HumanBeing].
 *
 * @property cool признак того, что автомобиль крутой
 */
@Serializable
data class Car(
    val cool: Boolean
)
=== ./common/src/main/kotlin/models/Coordinates.kt ===
package models

import kotlinx.serialization.Serializable

/**
 * Координаты местоположения [HumanBeing].
 *
 * @property x координата X
 * @property y координата Y
 */

@Serializable
data class Coordinates(
    val x: Long,
    val y: Int
)
=== ./common/src/main/kotlin/models/WeaponType.kt ===
package models

import kotlinx.serialization.Serializable

/**
 * Перечисление типов оружия, которым может владеть [HumanBeing].
 * Поле [weaponType] в [HumanBeing] может быть null.
 */
@Serializable
enum class WeaponType {
    HAMMER,
    SHOTGUN,
    BAT;
}
=== ./common/src/main/kotlin/models/HumanBeing.kt ===
package models

import kotlinx.serialization.Serializable
import network.utils.LocalDateSerializer
import network.utils.UUIDSerializer
import java.time.LocalDate
import java.util.UUID

/**
 * Основная сущность коллекции — Райан Гослинг.
 *
 * @property id уникальный идентификатор, генерируется автоматически
 * @property name имя героя (Райан Гослинг), не может быть null или пустой строкой
 * @property coordinates координаты героя, не может быть null
 * @property creationDate дата создания записи, генерируется автоматически
 * @property realHero является ли настоящим героем (конечно)
 * @property hasToothpick есть ли зубочистка (естественно)
 * @property impactSpeed скорость удара, не может быть null
 * @property soundtrackName название саундтрека, не может быть null (Nightcall - Kavinsky)
 * @property minutesOfWaiting количество минут ожидания
 * @property weaponType тип оружия, может быть null
 * @property car автомобиль героя, не может быть null
 */
@Serializable
data class HumanBeing(
    @Serializable(with = UUIDSerializer::class)
    var id: UUID? = null,
    @Serializable(with = UUIDSerializer::class)
    val ownerId: UUID,
    val name: String,
    val coordinates: Coordinates,
    @Serializable(with = LocalDateSerializer::class)
    val creationDate: LocalDate = LocalDate.now(),
    val realHero: Boolean,
    val hasToothpick: Boolean,
    val impactSpeed: Double,
    val soundtrackName: String,
    val minutesOfWaiting: Float,
    val weaponType: WeaponType?,
    val car: Car
) : Comparable<HumanBeing> {

    init {
        require(name.isNotBlank()) { "Имя не может быть пустым" }
    }

    /**
     * Сравниваем по имени и id
     */
    override fun compareTo(other: HumanBeing): Int {
        val byName = this.name.compareTo(other.name)
        if (byName != 0) return byName
        return this.id.toString().compareTo(other.id.toString())
    }

    /**
     * Два объекта считаются одинаковыми, если совпадают их [id].
     */
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is HumanBeing) return false
        return id == other.id
    }

    override fun hashCode(): Int = id.hashCode()

    /**
     * Красивое строковое представление для команды [show].
     */
    override fun toString(): String =
        """
        |HumanBeing (It Drives) {
        |  owner          = $ownerId
        |  id             = $id
        |  name           = $name (a.k.a. Ryan Gosling)
        |  coordinates    = (x=${coordinates.x}, y=${coordinates.y})
        |  creationDate   = $creationDate
        |  realHero       = $realHero
        |  hasToothpick   = $hasToothpick
        |  impactSpeed    = $impactSpeed
        |  soundtrackName = $soundtrackName
        |  minutesOfWait  = $minutesOfWaiting
        |  weaponType     = ${weaponType ?: "null"}
        |  car.cool       = ${car.cool}
        |}
        """.trimMargin()
}
=== ./common/src/main/kotlin/commands/CommandArgType.kt ===
package commands

import kotlinx.serialization.Serializable

// why there's only two types? yes.
@Serializable
enum class CommandArgType {
    STRING,
    NONE
}
=== ./common/src/main/kotlin/commands/Command.kt ===
package commands

import models.HumanBeing
import network.Response

interface Command {
    val name: String
    val description: String
    val args: List<CommandArgType>
    val type: CommandType

    fun execute(args: List<String>, humanBeing: HumanBeing? = null, userId: String = ""): Response
}
=== ./common/src/main/kotlin/commands/CommandType.kt ===
package commands

enum class CommandType {
    SIMPLE,
    MODEL
}=== ./client/src/main/kotlin/input/IOManager.kt ===
package input

import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.history.DefaultHistory
import org.jline.terminal.TerminalBuilder
import java.io.File
import java.io.FileNotFoundException
import java.util.ArrayDeque
import java.util.Scanner

class IOManager {
    val isInteractive: Boolean = true

    private val terminal = TerminalBuilder.builder()
        .system(true)
        .build()

    private val history = DefaultHistory()

    private val lineReader: LineReader = LineReaderBuilder.builder()
        .terminal(terminal)
        .history(history)
        // .completer(StringsCompleter(invoker.getCommandNames()))
        .option(LineReader.Option.HISTORY_IGNORE_DUPS, true)
        .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
        .build()

    private val scriptQueue = ArrayDeque<Scanner>()
    private val fileHistory = ArrayDeque<String>()

    /**
     * Читает строку.
     * Если есть активный скрипт — читает из него.
     * Иначе — интерактивный ввод через JLine.
     */
    fun readLine(prompt: String): String? {
        // Режим скрипта
        while (scriptQueue.isNotEmpty()) {
            val scanner = scriptQueue.peek()
            if (scanner.hasNextLine()) {
                return scanner.nextLine()
            }
            scanner.close()
            scriptQueue.pop()
            fileHistory.pop()
        }

        return try {
            lineReader.readLine(prompt)
        } catch (e: UserInterruptException) {
            ""
        } catch (e: EndOfFileException) {
            null
        }
    }

    fun addScriptScanner(filePath: String) {
        if (fileHistory.contains(filePath)) {
            printErrConsole("Скрипт уже был выполнен: $filePath")
            return
        }
        try {
            fileHistory.push(filePath)
            scriptQueue.push(Scanner(File(filePath), Charsets.UTF_8))
        } catch (e: FileNotFoundException) {
            printErrConsole("Файл не найден: $filePath")
        }
    }

    fun print(text: String) = println(text)

    fun printErrConsole(text: String) = System.err.println(text)
}=== ./client/src/main/kotlin/input/HumanBeingBuilder.kt ===
package input

import models.Car
import models.Coordinates
import models.HumanBeing
import models.WeaponType
import network.NetworkManager
import java.util.UUID

/**
 * Интерактивный построитель объекта [HumanBeing].
 *
 * Читает поля по одному из [inputManager], показывает приглашения,
 * валидирует ввод и повторяет запрос при ошибке.
 *
 * Поля [HumanBeing.id] и [HumanBeing.creationDate] генерируются
 * автоматически и не запрашиваются у пользователя.
 *
 * @property inputManager источник ввода
 */

class HumanBeingBuilder(
    private val inputManager: IOManager,
    private val networkManager: NetworkManager
) {

    /**
     * Запускает интерактивный ввод всех полей и возвращает готовый [HumanBeing].
     *
     * @throws IllegalStateException если EOF
     */
    fun build(): HumanBeing {
        val name = readNonBlankString("Введите имя:")
        val coordinates = readCoordinates()
        val realHero = readBoolean("Герой? (true/false):")
        val hasToothpick = readBoolean("Есть зубочистка? (true/false):")
        val impactSpeed = readDouble("Введите скорость удара (impactSpeed):")
        val soundtrackName = readNonBlankString("Введите название саундтрека:")
        val minutesOfWaiting = readFloat("Введите минуты ожидания (minutesOfWaiting):")
        val weaponType = readNullableWeaponType()
        val car = readCar()

        return HumanBeing(
            name = name, coordinates = coordinates, realHero = realHero,
            hasToothpick = hasToothpick, impactSpeed = impactSpeed,
            soundtrackName = soundtrackName, minutesOfWaiting = minutesOfWaiting,
            weaponType = weaponType, car = car, ownerId = networkManager.getUUID()
        )
    }

    private fun readCoordinates(): Coordinates {
        val x = readLong("Введите координату X (тип Long):")
        val y = readInt("Введите координату Y (тип Int):")
        return Coordinates(x, y)
    }

    private fun readCar(): Car {
        val cool = readBoolean("Крутой автомобиль ? (true/false):")
        return Car(cool)
    }

    /**
     * Читает непустую строку. Повторяет запрос при пустом вводе.
     */
    private fun readNonBlankString(prompt: String): String {
        while (true) {
            val input = inputManager.readLine(" > $prompt")
                ?: throw IllegalStateException("EOF")
            if (input.isNotBlank()) return input.trim()
            inputManager.print("[Ошибка] Строка не может быть пустой.")
        }
    }

    /**
     * Читает Long. Повторяет запрос при некорректном вводе.
     */
    private fun readLong(prompt: String): Long {
        while (true) {
            val input = inputManager.readLine(" > $prompt")
                ?: throw IllegalStateException("EOF")
            try {
                return input.trim().toLong()
            } catch (e: NumberFormatException) {
                inputManager.print("[Ошибка] Ожидается целое число (Long). Попробуйте снова.")
            }
        }
    }

    /**
     * Читает Int. Повторяет запрос при некорректном вводе.
     */
    private fun readInt(prompt: String): Int {
        while (true) {
            val input = inputManager.readLine(" > $prompt")
                ?: throw IllegalStateException("EOF")
            try {
                return input.trim().toInt()
            } catch (e: NumberFormatException) {
                inputManager.print("[Ошибка] Ожидается целое число (Int). Попробуйте снова.")
            }
        }
    }

    /**
     * Читает Double. Повторяет запрос при некорректном вводе.
     */
    private fun readDouble(prompt: String): Double {
        while (true) {
            val input = inputManager.readLine(" > $prompt")
                ?: throw IllegalStateException("EOF")
            try {
                return input.trim().toDouble()
            } catch (e: NumberFormatException) {
                inputManager.print("[Ошибка] Ожидается число с плавающей точкой (Double). Попробуйте снова.")
            }
        }
    }

    /**
     * Читает Float. Повторяет запрос при некорректном вводе.
     */
    private fun readFloat(prompt: String): Float {
        while (true) {
            val input = inputManager.readLine(" > $prompt")
                ?: throw IllegalStateException("EOF")
            try {
                return input.trim().toFloat()
            } catch (e: NumberFormatException) {
                inputManager.print("[Ошибка] Ожидается число (Float). Попробуйте снова.")
            }
        }
    }

    /**
     * Читает Boolean (true/false). Повторяет запрос при некорректном вводе.
     */
    private fun readBoolean(prompt: String): Boolean {
        while (true) {
            val input = inputManager.readLine(" > $prompt")
                ?: throw IllegalStateException("EOF")
            when (input.trim().lowercase()) {
                "true"  -> return true
                "false" -> return false
                else    -> inputManager.print("[Ошибка] Ожидается 'true' или 'false'. Попробуйте снова.")
            }
        }
    }

    /**
     * Читает [WeaponType] или null (пустая строка).
     * Перед вводом выводит список допустимых констант.
     */
    private fun readNullableWeaponType(): WeaponType? {
        val constants = WeaponType.entries.joinToString(", ")
        while (true) {
            val input = inputManager.readLine(" > Введите тип оружия (доступные: $constants) или оставьте пустым для null:")
                ?: throw IllegalStateException("EOF")
            if (input.isBlank()) return null
            try {
                return WeaponType.valueOf(input.trim().uppercase())
            } catch (e: IllegalArgumentException) {
                inputManager.print("[Ошибка] Неизвестный тип оружия '$input'. Допустимые значения: $constants")
            }
        }
    }

    /**
     * Выводит приглашение к вводу только в интерактивном режиме.
     * В режиме скрипта приглашения не выводятся.
     */
    private fun printPrompt(message: String) {
        if (inputManager.isInteractive) inputManager.print(" > $message ")
    }
}
=== ./client/src/main/kotlin/runner/CommandInvoker.kt ===
package runner

import commands.ClientCommand
import commands.CommandType
import input.HumanBeingBuilder
import input.IOManager
import network.NetworkManager

class CommandInvoker(
    private val io: IOManager,
    private val networkManager: NetworkManager
) {
    private val commands = mutableMapOf<String, ClientCommand>()
    private val history = ArrayDeque<String>(12)

    fun register(command: ClientCommand) {
        commands[command.name] = command
    }

    fun execute(input: String) {
        val parts = input.trim().split(" ")
        val name = parts[0].lowercase().trim()
        val args = parts.drop(1)

        val command = commands[name]
        if (command == null) {
            io.print("Неизвестная команда: '$name'. Введите 'help' для справки.")
            return
        }

        addToHistory(name)
        try {
            if (command.type == CommandType.MODEL) {
                val model = HumanBeingBuilder(io, networkManager).build()
                val res = command.execute(args, model)
                io.print(res)
            } else {
                val res = command.execute(args)
                io.print(res)
            }
        } catch (e: IllegalStateException) {
            io.print("[Ошибка выполнения] ${e.message}")
        }
    }

    fun getHistory(): List<String> = history.toList()

    fun getCommands(): Map<String, ClientCommand> = commands.toMap()

    private fun addToHistory(name: String) {
        if (history.size >= 12) history.removeFirst()
        history.addLast(name)
    }

    fun getCommandNames(): List<String> = commands.keys.toList()
}
=== ./client/src/main/kotlin/network/NetworkManager.kt ===
package network

import kotlinx.serialization.SerializationException
import java.net.InetSocketAddress
import java.net.PortUnreachableException
import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel
import java.util.UUID

class NetworkManager(
    private val host: String,
    private val port: Int,
    private val bufferSize: Int = 65507,
    private val timeoutMS: Long  = 3000,
    private val retryDelayMS: Long = 5000,
    private val maxRetries: Int = 3,
) {

    private var token: String = "no token"
    private var id: String = " no id"

    private var channel: DatagramChannel = openChannel()

    private fun openChannel(): DatagramChannel =
        DatagramChannel.open().also {
            it.configureBlocking(false)
            it.connect(InetSocketAddress(host, port))
        }

    fun sendRequest(authRequest: AuthRequest): AuthResponse? {
        val requestBytes = AppJson.encodeToString(AuthRequest.serializer(), authRequest)
            .toByteArray(Charsets.UTF_8)

        repeat(maxRetries) { attempt ->
            try {
                channel.write(ByteBuffer.wrap(requestBytes))
                val response = waitForAuthResponse()
                if (response != null) return response
            } catch (e: PortUnreachableException) {
                reconnect()
            } catch (e: Exception) {
                reconnect()
            }

            println(
                "[Client] Сервер не отвечает " +
                        "(попытка ${attempt + 1}/$maxRetries). " +
                        "Повтор через ${retryDelayMS / 1000} сек..."
            )
            Thread.sleep(retryDelayMS)
        }

        println("[Client] Сервер недоступен. Вход не выполнен.")
        return null
    }

    fun sendRequest(request: Request): Response? {
        val requestBytes = AppJson.encodeToString(Request.serializer(), request)
            .toByteArray(Charsets.UTF_8)

        repeat(maxRetries) { attempt ->
            try {
                channel.write(ByteBuffer.wrap(requestBytes))
                val response = waitForResponse()
                if (response != null) return response
            } catch (e: PortUnreachableException) {
                reconnect()
            } catch (e: Exception) {
                reconnect()
            }

            println(
                "[Client] Сервер не отвечает " +
                        "(попытка ${attempt + 1}/$maxRetries). " +
                        "Повтор через ${retryDelayMS / 1000} сек..."
            )
            Thread.sleep(retryDelayMS)
        }

        println("[Client] Сервер недоступен. Команда не выполнена.")
        return null
    }

    private fun waitForResponse(): Response? {
        val buf = ByteBuffer.allocate(bufferSize)
        val deadline = System.currentTimeMillis() + timeoutMS

        while (System.currentTimeMillis() < deadline) {
            try {
                buf.clear()
                if (channel.read(buf) > 0) {
                    buf.flip()
                    return AppJson.decodeFromString<Response>(
                        Charsets.UTF_8.decode(buf).toString()
                    )
                }
            } catch (e: PortUnreachableException) {
                return null
            } catch (e: SerializationException) {
                System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
                return null
            }
            Thread.sleep(50)
        }
        return null
    }

    private fun waitForAuthResponse(): AuthResponse? {
        val buf = ByteBuffer.allocate(bufferSize)
        val deadline = System.currentTimeMillis() + timeoutMS

        while (System.currentTimeMillis() < deadline) {
            try {
                buf.clear()
                if (channel.read(buf) > 0) {
                    buf.flip()
                    return AppJson.decodeFromString<AuthResponse>(
                        Charsets.UTF_8.decode(buf).toString()
                    )
                }
            } catch (e: PortUnreachableException) {
                return null
            } catch (e: SerializationException) {
                System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
                return null
            }
            Thread.sleep(50)
        }
        return null
    }

    private fun reconnect() {
        try { channel.close() } catch (_: Exception) {}
        channel = openChannel()
    }

    fun close() {
        try { channel.close() } catch (_: Exception) {}
    }

    fun setToken(newToken: String) {
        token = newToken
    }

    fun getToken(): String = token

    fun clearToken() {
        token = "no token"
    }

    fun setUUID(newToken: String) {
        id = newToken
    }

    fun getUUID(): UUID {
        return UUID.fromString(id)
    }
}
=== ./client/src/main/kotlin/Main.kt ===

import commands.AuthCommand
import commands.ExecuteScriptCommand
import commands.ExitCommand
import commands.HelpCommand
import commands.LogoutCommand
import commands.SyncCommand
import network.NetworkManager
import input.IOManager
import runner.CommandInvoker

fun main(args: Array<String>) {
    val host = if (args.isNotEmpty()) args[0] else "localhost"
    val port = if (args.size >= 2) args[1].toIntOrNull() ?: 8080 else 8080

    val io = IOManager()                        // Name by Boris Bosenko
    val client = NetworkManager(host, port)
    val invoker = CommandInvoker(io, client)

    // Shutdown hook
    Runtime.getRuntime().addShutdownHook(Thread {
        client.close()
    })

    // do auth stuff
    listOf(
        HelpCommand(invoker),
        AuthCommand(networkManager = client),
    ).forEach { invoker.register(it)}

    while (client.getToken() == "no token") {
        val line = io.readLine(" ☭ ") ?: break
        if (line.isBlank()) continue
        invoker.execute(line)
    }

    io.print("""
        |════════════════════════════════════════════
        |Lab6 Client — подключение к $host:$port  
        |════════════════════════════════════════════
            """.trimMargin())

    registerBasicCommands(invoker, client, io)
    runClientRepl(io, invoker)
}

fun registerBasicCommands(invoker: CommandInvoker,client: NetworkManager, io: IOManager) {
    listOf(
        ExecuteScriptCommand(io),
        SyncCommand(invoker, client),
        LogoutCommand(client)
    ).forEach { invoker.register(it)}
}

fun runClientRepl(io: IOManager, invoker : CommandInvoker) {
    // sync commands w server
    invoker.execute("sync")

    var isRunning = true
    invoker.register(ExitCommand {isRunning = false})

    while (isRunning) {
        val line = io.readLine(" ☭ ") ?: break
        if (line.isBlank()) continue
        invoker.execute(line)
    }

    io.print("До свидания!")
}
=== ./client/src/main/kotlin/commands/ExitCommand.kt ===
package commands

import models.HumanBeing

class ExitCommand(private val onExit: () -> Unit) : ClientCommand {
    override val name = "exit"
    override val description = "завершить клиент"
    override val args: List<CommandArgType> = listOf(CommandArgType.NONE)
    override val type = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
        onExit()
        return ""
    }
}=== ./client/src/main/kotlin/commands/AuthCommand.kt ===
package commands

import models.HumanBeing
import network.AuthRequest
import network.AuthResponse
import network.AuthType
import network.NetworkManager
import java.security.MessageDigest

class AuthCommand(
    private val networkManager: NetworkManager,
): ClientCommand {
    override val name: String = "auth"
    override val description: String = "вход или регистрация. auth login <username> <password> or auth register <username> <password>"
    override val args: List<CommandArgType> = listOf(CommandArgType.NONE)
    override val type: CommandType = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
        if (args.isEmpty()) return "[Ошибка] Укажите действие, логин и пароль. Пример: auth login <username> <password>"
        if (args[1].isEmpty() || args[2].isEmpty()) return "[Ошибка] Укажите логин и пароль. Пример: auth login <username> <password>"

        val res = if (args[0] == "login") {
            networkManager.sendRequest(AuthRequest(args[1], md5Hash(args[2]), AuthType.LOGIN))
        } else if (args[0] == "register") {
            networkManager.sendRequest(AuthRequest(args[1], md5Hash(args[2]), AuthType.REGISTER))
        } else {
            null
        }

        if (res == null) return "[!] Сервер не отвечает."
        if (res.success) {
            networkManager.setToken(res.accessToken!!)
            networkManager.setUUID(res.id!!)
        }
        return res.message
    }

    private fun md5Hash(input: String): String {
        val md = MessageDigest.getInstance("MD5")
        val hashBytes = md.digest(input.toByteArray())
        return hashBytes.joinToString("") { String.format("%02x", it) }
    }
}=== ./client/src/main/kotlin/commands/LogoutCommand.kt ===
package commands

import models.HumanBeing
import network.NetworkManager

class LogoutCommand(private val networkManager: NetworkManager): ClientCommand {
    override val name: String = "logout"
    override val description: String = "выход из аккаунта"
    override val args: List<CommandArgType> = listOf(CommandArgType.NONE)
    override val type: CommandType = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
        networkManager.clearToken()
        return "Пользователь вышел. Войдите заново"
    }
}=== ./client/src/main/kotlin/commands/ServerCommand.kt ===
package commands

import models.HumanBeing
import network.Request
import network.NetworkManager

class ServerCommand(
    override var name: String,
    override var description: String,
    override var args: List<CommandArgType>,
    override var type: CommandType,
    private val networkManager: NetworkManager,
): ClientCommand {

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
        val request = Request(
            commandName = this.name,
            args = args,
            humanBeing = humanBeing,
            jwt = networkManager.getToken()
        )
        val response = networkManager.sendRequest(request)
        return response?.message ?: "[!] Сервер не ответил"
    }
}=== ./client/src/main/kotlin/commands/HelpCommand.kt ===
package commands

import models.HumanBeing
import runner.CommandInvoker

class HelpCommand(private val invoker: CommandInvoker) : ClientCommand {
    override val name = "help"
    override val description = "вывести справку по доступным командам"
    override val args = listOf(CommandArgType.NONE)
    override val type = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String =
        invoker.getCommands().values
            .sortedBy { it.name }
            .joinToString("\n") { "  ${it.name.padEnd(40)} — ${it.description}" }
}=== ./client/src/main/kotlin/commands/ExecuteScriptCommand.kt ===
package commands

import input.IOManager
import models.HumanBeing

class ExecuteScriptCommand(private val io: IOManager) : ClientCommand {
    override val name = "execute_script"
    override val description = "Выполняет скрипт из файла"
    override val args = listOf(CommandArgType.STRING)
    override val type = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
        io.addScriptScanner(args[0])
        return ""
    }
}=== ./client/src/main/kotlin/commands/ClientCommand.kt ===
package commands

import models.HumanBeing

interface ClientCommand {
    val name: String
    val description: String
    val args: List<CommandArgType>
    val type: CommandType

    fun execute(args: List<String>, humanBeing: HumanBeing? = null): String

}=== ./client/src/main/kotlin/commands/SyncCommand.kt ===
package commands

import models.HumanBeing
import network.NetworkManager
import network.Request
import runner.CommandInvoker

class SyncCommand (
    private val invoker: CommandInvoker,
    private val client: NetworkManager,
) : ClientCommand {
    override val name = "sync"
    override val description = "получить команды с сервера"
    override val args = listOf(CommandArgType.NONE)
    override val type = CommandType.SIMPLE

    override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
        val initialRequest = Request(
            commandName = "sync",
            humanBeing = null,
            jwt = client.getToken(),
        )
        val res = client.sendRequest(initialRequest)

        if (res == null) {
            return ("[!] Сервер недоступен. Попробуйте позже.")
        } else {
            res.commands?.forEach { serverCommand ->
                invoker.register(
                    ServerCommand(
                        name = serverCommand.name,
                        description = serverCommand.description,
                        type = serverCommand.type,
                        args = serverCommand.args,
                        networkManager = client
                    )
                )
            }
         return ("| Подключен. | Введите 'help' для справки.")
        }
    }
}=== ./gateway/src/main/kotlin/core/GatewayManager.kt ===
package core

import network.AppJson
import network.Request
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executors
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.RecursiveAction
import java.util.concurrent.RecursiveTask
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

data class BackendServer(val host: String, val port: Int) {
    @Volatile var isAlive: Boolean = false
    override fun toString() = "$host:$port"
}

class GatewayManager(
    private val listenPort: Int,
    private val backendsConfig: String,
    private val healthCheckIntervalMs: Long = 5_000L,
    private val discoveryIntervalMs: Long = 15_000L,
    private val probeTimeoutMs: Int = 2_000
) {
    private val backends = CopyOnWriteArrayList<BackendServer>()
    private val socket = DatagramSocket(listenPort)
    private val roundRobinCounter = AtomicInteger(0)

    private val readerPool     = Executors.newFixedThreadPool(4)
    private val processingPool = ForkJoinPool(Runtime.getRuntime().availableProcessors())
    private val senderPool     = ForkJoinPool(Runtime.getRuntime().availableProcessors())
    private val healthPool     = Executors.newScheduledThreadPool(2)
    private val discoveryPool  = Executors.newSingleThreadScheduledExecutor()

    fun start() {
        println("[Gateway] Listening on port $listenPort")
        println("[Gateway] Backend config: $backendsConfig")
        discoverBackends()
        startHealthChecks()
        startDiscovery()
        repeat(4) { readerPool.submit(::readLoop) }
        try {
            Thread.currentThread().join()
        } catch (_: InterruptedException) {}
    }

    private fun resolveBackends(): List<BackendServer> {
        return if (backendsConfig.contains(",")) {
            backendsConfig.split(",").map { s ->
                val parts = s.trim().split(":")
                BackendServer(parts[0], parts[1].toInt())
            }
        } else {
            val parts = backendsConfig.split(":")
            val hostname = parts[0]
            val port = parts[1].toInt()
            try {
                InetAddress.getAllByName(hostname).map {
                    BackendServer(it.hostAddress, port)
                }
            } catch (e: Exception) {
                println("[Gateway] DNS resolution failed for $hostname: ${e.message}")
                emptyList()
            }
        }
    }

    private fun discoverBackends() {
        val resolved = resolveBackends()
        if (resolved.isEmpty()) return

        val existingByAddr = backends.associateBy { "${it.host}:${it.port}" }
        val newBackends = resolved.filter { "${it.host}:${it.port}" !in existingByAddr }
        val aliveCount = resolved.count { existingByAddr["${it.host}:${it.port}"]?.isAlive == true }

        resolved.forEach { newB ->
            existingByAddr["${newB.host}:${newB.port}"]?.let {
                newB.isAlive = it.isAlive
            }
        }

        backends.removeAll { "${it.host}:${it.port}" !in resolved.map { "${it.host}:${it.port}" }.toSet() }
        backends.addAllAbsent(newBackends)

        if (newBackends.isNotEmpty())
            println("[Gateway] Discovered ${newBackends.size} new backend(s): ${newBackends.joinToString()}")
        println("[Gateway] Backends: ${backends.size} total, $aliveCount alive")
    }

    private fun startDiscovery() {
        discoveryPool.scheduleAtFixedRate(
            { discoverBackends() },
            discoveryIntervalMs,
            discoveryIntervalMs,
            TimeUnit.MILLISECONDS
        )
    }

    private fun startHealthChecks() {
        println("[Gateway] Initial health check...")
        backends.forEach { probeServer(it) }
        println("[Gateway] Alive: ${backends.filter { it.isAlive }.map { "$it" }}")

        healthPool.scheduleAtFixedRate(
            { backends.forEach { probeServer(it) } },
            healthCheckIntervalMs,
            healthCheckIntervalMs,
            TimeUnit.MILLISECONDS
        )
    }

    private fun probeServer(backend: BackendServer) {
        val wasAlive = backend.isAlive
        backend.isAlive = try {
            val probe = AppJson.encodeToString(
                Request.serializer(),
                Request(commandName = "info", jwt = "no token")
            ).toByteArray(Charsets.UTF_8)

            DatagramSocket().use { bs ->
                bs.soTimeout = probeTimeoutMs
                bs.send(DatagramPacket(probe, probe.size, InetAddress.getByName(backend.host), backend.port))
                val buf = ByteArray(65507)
                bs.receive(DatagramPacket(buf, buf.size))
                true
            }
        } catch (e: Exception) {
            false
        }

        if (wasAlive != backend.isAlive)
            println("[Gateway] $backend -> ${if (backend.isAlive) "UP" else "DOWN"}")
    }

    private fun pickBackend(): BackendServer? {
        val alive = backends.filter { it.isAlive }
        if (alive.isEmpty()) return null
        return alive[roundRobinCounter.getAndIncrement() % alive.size]
    }

    private fun readLoop() {
        val buf = ByteArray(65507)
        while (!socket.isClosed) {
            try {
                val packet = DatagramPacket(buf.copyOf(), buf.size)
                socket.receive(packet)

                val address   = packet.address
                val clientPort = packet.port
                val data      = packet.data.copyOf(packet.length)

                processingPool.submit(ProxyTask(data, address, clientPort))
            } catch (e: Exception) {
                if (!socket.isClosed) println("[Gateway] Read error: ${e.message}")
                else break
            }
        }
    }

    inner class ProxyTask(
        private val data: ByteArray,
        private val clientAddress: InetAddress,
        private val clientPort: Int
    ) : RecursiveTask<Unit>() {

        override fun compute() {
            val backend = pickBackend()
            if (backend == null) {
                val err = """{"success":false,"message":"No alive servers"}"""
                    .toByteArray(Charsets.UTF_8)
                senderPool.submit(SendTask(err, clientAddress, clientPort))
                return
            }

            val commandName = try {
                AppJson.decodeFromString<Request>(data.decodeToString()).commandName
            } catch (_: Exception) { "?" }
            println("[Gateway] '$commandName' from $clientAddress:$clientPort → $backend")

            try {
                DatagramSocket().use { bs ->
                    bs.soTimeout = probeTimeoutMs
                    bs.send(DatagramPacket(data, data.size, InetAddress.getByName(backend.host), backend.port))

                    val respBuf = ByteArray(65507)
                    val respPacket = DatagramPacket(respBuf, respBuf.size)
                    bs.receive(respPacket)

                    senderPool.submit(
                        SendTask(respPacket.data.copyOf(respPacket.length), clientAddress, clientPort)
                    )
                }
            } catch (e: Exception) {
                println("[Gateway] Backend $backend failed: ${e.message}")
                backend.isAlive = false

                val fallback = pickBackend()
                if (fallback != null) {
                    processingPool.submit(ProxyTask(data, clientAddress, clientPort))
                } else {
                    val err = """{"success":false,"message":"All backends unavailable"}"""
                        .toByteArray(Charsets.UTF_8)
                    senderPool.submit(SendTask(err, clientAddress, clientPort))
                }
            }
        }
    }

    inner class SendTask(
        private val bytes: ByteArray,
        private val address: InetAddress,
        private val port: Int
    ) : RecursiveAction() {

        override fun compute() {
            try {
                val packet = DatagramPacket(bytes, bytes.size, address, port)
                synchronized(socket) { socket.send(packet) }
            } catch (e: Exception) {
                println("[Gateway] Send error: ${e.message}")
            }
        }
    }

    fun stop() {
        println("[Gateway] Shutting down...")
        socket.close()
        readerPool.shutdownNow()
        processingPool.shutdown()
        senderPool.shutdown()
        healthPool.shutdownNow()
        discoveryPool.shutdownNow()
    }
}
=== ./gateway/src/main/kotlin/Main.kt ===
import core.GatewayManager

fun main(args: Array<String>) {
    val listenPort = if (args.isNotEmpty()) args[0].toInt() else 9090
    val backends = System.getenv("BACKENDS") ?: "localhost:8080,localhost:8081,localhost:8082"

    val gateway = GatewayManager(
        listenPort = listenPort,
        backendsConfig = backends,
        healthCheckIntervalMs = 5_000L,
        discoveryIntervalMs = 15_000L,
        probeTimeoutMs = 7_000
    )

    Runtime.getRuntime().addShutdownHook(Thread {
        gateway.stop()
    })

    gateway.start()
}
